ThinkGeo Core Architecture Guide¶
Version: Covers ThinkGeo.Core v12–v14 (NuGet package
ThinkGeo.Core)
Platforms: WPF · WinForms · MAUI · Blazor · GIS Server
Last updated: March 2026
Table of Contents¶
- Package & Namespace Structure
- The Core Object Model
- The Five-Layer Stack
- The Rendering Pipeline
- Coordinate Systems & Projections
- Styling System
- Platform Targets
- Common Patterns & Recipes
- Performance Checklist
- Common Errors & Fixes
1. Package & Namespace Structure¶
ThinkGeo separates its core GIS engine from its UI bindings. You always depend on the core, then add a platform-specific UI package on top.
| NuGet Package | Namespace | Purpose |
|---|---|---|
ThinkGeo.Core | ThinkGeo.Core | GIS engine: layers, styles, geometry, projections, spatial queries |
ThinkGeo.UI.Wpf | ThinkGeo.UI.Wpf | WPF MapView control |
ThinkGeo.UI.WinForms | ThinkGeo.UI.WinForms | WinForms MapView control |
ThinkGeo.UI.Maui | ThinkGeo.UI.Maui | .NET MAUI MapView control (iOS, Android, Mac, Windows) |
ThinkGeo.UI.Blazor | ThinkGeo.UI.Blazor | Blazor MapView component |
ThinkGeo.GisServer | ThinkGeo.GisServer | Server-side OGC service host (WMS, WMTS, WFS, XYZ, GeoJSON) |
Rule of thumb: All GIS logic (layers, styles, features, projections) lives in
ThinkGeo.Coreand is shared across every platform. The UI packages only provide the map control that renders it.
2. The Core Object Model¶
The object model has five nested levels. Understanding these relationships is the single most important thing a new developer needs to know.
MapView
└── Overlays (ordered collection)
├── LayerOverlay ← contains vector/raster Feature Layers
│ └── Layers (ordered collection)
│ └── FeatureLayer ← wraps a FeatureSource
│ └── FeatureSource ← actual data access (shapefile, SQL, in-memory…)
│ └── ProjectionConverter ← optional on-the-fly reprojection
└── ThinkGeoCloudVectorMapsOverlay ← background tile service (a special overlay)
MapView¶
The MapView is the root UI control. It owns everything rendered on screen.
| Key property | Type | Meaning |
|---|---|---|
MapUnit | GeographyUnit | The CRS of the map canvas itself — almost always GeographyUnit.Meter (EPSG:3857) |
CurrentScale | double | Current denominator scale (e.g. 9400 = 1:9400) |
CenterPoint | PointShape | Center of the viewport in map units |
Overlays | OverlayCollection | Ordered list of overlays, rendered back-to-front |
mapView.MapUnit = GeographyUnit.Meter;
mapView.CenterPoint = new PointShape(-10777290, 3908740); // Spherical Mercator
mapView.CurrentScale = 9400;
Overlay¶
An Overlay is a compositing unit — it renders independently onto its own bitmap and gets alpha-composited into the final frame. There are two main kinds:
LayerOverlay— holds one or moreLayerobjects. This is what you use for your own data.- Background overlays —
ThinkGeoCloudVectorMapsOverlay,BingMapsOverlay,WmsRasterLayer-based overlays, etc. Add these first so they sit behind your data.
// Background first
var cloudOverlay = new ThinkGeoCloudVectorMapsOverlay
{
ClientId = "your-client-id",
ClientSecret = "your-client-secret",
MapType = ThinkGeoCloudVectorMapsMapType.Light,
TileCache = new FileRasterTileCache(@".\cache", "thinkgeo_light")
};
mapView.Overlays.Add(cloudOverlay);
// Your data on top
var myOverlay = new LayerOverlay();
mapView.Overlays.Add("myData", myOverlay);
LayerOverlay has two tile modes:
TileType | Behaviour | Use when |
|---|---|---|
MultipleTiles (default) | Divides the canvas into 256×256 tiles, renders concurrently | Fast panning/zooming, large datasets |
SingleTile | Renders the entire viewport as one image | Simple scenes, XAML-declared maps |
Layer¶
A Layer is a source of visual content. There are two major families:
FeatureLayer— vector data. Subclasses includeShapeFileFeatureLayer,SqlServerFeatureLayer,MsSqliteFeatureLayer,InMemoryFeatureLayer,GeoJsonFeatureLayer, and many more.- Raster layers —
GeoImageLayer,WmsRasterLayer,GeoTiffRasterLayer, etc.
All layers must be opened before drawing and closed when done (handled automatically during RefreshAsync, but relevant when querying data directly).
var hotelsLayer = new ShapeFileFeatureLayer(@"./Data/Hotels.shp");
myOverlay.Layers.Add("hotels", hotelsLayer);
FeatureSource¶
The FeatureSource is the actual data-access object sitting inside a FeatureLayer. You rarely construct it directly — it is created by the layer — but you interact with its properties:
// Attach a projection converter so data in EPSG:2276 (Texas State Plane)
// gets reprojected on the fly to the map's EPSG:3857
hotelsLayer.FeatureSource.ProjectionConverter = new ProjectionConverter(2276, 3857);
You can also query the FeatureSource directly without rendering:
hotelsLayer.Open();
var features = hotelsLayer.QueryTools.GetAllFeatures(ReturningColumnsType.AllColumns);
hotelsLayer.Close();
3. The Five-Layer Stack¶
When you set up a typical ThinkGeo map, you are building a five-layer visual stack:
┌─────────────────────────────────────────┐
│ 5. Interaction overlays │ EditOverlay, PopupOverlay, MeasureOverlay
│ 4. Your vector feature overlays │ LayerOverlay containing FeatureLayers
│ 3. Your raster data overlays │ LayerOverlay with WmsRasterLayer etc.
│ 2. Tile basemap overlay │ ThinkGeoCloudVectorMapsOverlay
│ 1. MapView canvas │ MapView (sets MapUnit, scale, center)
└─────────────────────────────────────────┘
Overlays are rendered in Overlays collection order — first-added is at the bottom. Add your background basemap first and your interactive layers last.
4. The Rendering Pipeline¶
When mapView.RefreshAsync() is called (or pan/zoom occurs), ThinkGeo runs this pipeline:
RefreshAsync()
│
├─► For each Overlay (in order):
│ │
│ └─► Overlay.DrawAsync(extent, canvas)
│ │
│ └─► [LayerOverlay] For each Layer:
│ │
│ ├─► Layer.IsDrawingNeeded(currentScale) ← zoom filter
│ │
│ └─► Layer.DrawCore(GeoCanvas, labelsInAllLayers)
│ │
│ └─► FeatureSource.GetFeaturesForDrawing(boundingBox, w, h, columns)
│ │
│ └─► [For each Feature]:
│ ZoomLevel.Draw(canvas, feature, labels)
│ └─► Style.Draw(feature, canvas)
│
└─► Composite all overlay bitmaps → screen
Key points:
GeoCanvasis the drawing surface abstraction. It wraps the platform's native graphics API (GDI+ on WinForms, Skia on MAUI, etc.). You never create aGeoCanvasdirectly; it is passed intoDrawCore.ZoomLevelcontrols which styles are active at each scale. ZoomLevel01 is the most zoomed-in (highest scale denominator), ZoomLevel20 is zoomed out.ApplyUntilZoomLevelcascades a style across a range.- Labels are accumulated in a shared
labelsInAllLayerscollection so cross-layer collision detection works correctly.
5. Coordinate Systems & Projections¶
The map's native CRS¶
MapView.MapUnit defines the coordinate system of the map canvas. For web-style tiled maps (including ThinkGeo Cloud Maps), this must be GeographyUnit.Meter, which corresponds to EPSG:3857 (Web Mercator / Spherical Mercator). All overlay coordinates, CenterPoint, and extent calculations use this unit.
Per-layer reprojection¶
Your data may be in a different CRS (e.g. EPSG:4326 decimal degrees, a national grid like EPSG:2276, etc.). Attach a ProjectionConverter to the layer's FeatureSource:
// Data is in Texas State Plane South (feet), map is in Web Mercator
layer.FeatureSource.ProjectionConverter = new ProjectionConverter(2276, 3857);
ProjectionConverter uses EPSG codes. The first argument is the source SRID, the second is the destination. The converter is opened and closed automatically with the layer.
A common mistake: forgetting to open the projection before calling it manually. If you query a FeatureSource directly, call Open() first — the projection must also be open:
layer.Open(); // opens both FeatureSource and ProjectionConverter
GeographyUnit¶
GeographyUnit | Meaning | Typical CRS |
|---|---|---|
Meter | Metric, Spherical Mercator | EPSG:3857 |
DecimalDegree | Lat/lon degrees | EPSG:4326 |
Feet | Survey feet | Many US state planes |
Unknown | Unknown / ignored | Raster without georef |
6. Styling System¶
Styles are attached to ZoomLevel objects inside a layer's ZoomLevelSet. Each ZoomLevel has a CustomStyles collection and convenience shorthand properties (DefaultPointStyle, DefaultLineStyle, DefaultAreaStyle, DefaultTextStyle).
Attaching a style¶
// Create the style
var circleStyle = new PointStyle(PointSymbolType.Circle, 12,
GeoBrushes.Blue,
new GeoPen(GeoBrushes.White, 2));
// Attach to ZoomLevel01 (most zoomed in)
layer.ZoomLevelSet.ZoomLevel01.CustomStyles.Clear();
layer.ZoomLevelSet.ZoomLevel01.CustomStyles.Add(circleStyle);
// Cascade this style to apply at all zoom levels
layer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
Built-in style types¶
| Style class | Geometry | Notes |
|---|---|---|
PointStyle | Points | Supports symbol, icon image, font glyph |
LineStyle | Lines/polylines | Stroke, dash patterns, end caps |
AreaStyle | Polygons | Fill and outline independently |
TextStyle | Any | Label from a column value |
ValueStyle | Any | Choropleth — maps column values to sub-styles |
ClassBreakStyle | Any | Range-based thematic styling |
ClusterPointStyle | Points | Clusters nearby points at lower zooms |
HeatStyle | Points | Heatmap rendering |
FilterStyle | Any | Applies sub-styles only when a SQL-like filter matches |
Icon and font-glyph styles¶
// Icon from file
var iconStyle = new PointStyle(new GeoImage(@"./Resources/hotel_icon.png"))
{
ImageScale = 0.25
};
// Font glyph
var glyphStyle = new PointStyle(new GeoFont("Verdana", 16, DrawingFontStyles.Bold), "@", GeoBrushes.Black)
{
Mask = new AreaStyle(GeoBrushes.White),
MaskType = MaskType.Circle
};
7. Platform Targets¶
All platforms share the same ThinkGeo.Core object model. The only thing that differs is the MapView control and its host framework.
WPF (ThinkGeo.UI.Wpf)¶
<!-- XAML declaration (single-tile mode) -->
<tgWpf:MapView x:Name="MapView"
MapUnit="Meter"
Loaded="MapView_Loaded"
DefaultOverlaysRenderSequenceType="Concurrent">
<tgWpf:MapView.Overlays>
<tgWpf:LayerOverlay TileType="SingleTile">
<tgWpf:LayerOverlay.Layers>
<tgCore:ShapeFileFeatureLayer ShapePathFilename="Data/Countries.shp"/>
</tgWpf:LayerOverlay.Layers>
</tgWpf:LayerOverlay>
</tgWpf:MapView.Overlays>
</tgWpf:MapView>
Most configuration still happens in code-behind in MapView_Loaded.
WinForms (ThinkGeo.UI.WinForms)¶
The API is identical to WPF but the control is added to a Form or UserControl. Configuration always happens in code (no XAML):
private void Form_Load(object sender, EventArgs e)
{
mapView1.MapUnit = GeographyUnit.Meter;
// ... same pattern as WPF
}
.NET MAUI (ThinkGeo.UI.Maui)¶
Supports iOS, Android, macOS, and Windows from one codebase. XAML and code-behind follow the same pattern as WPF, with the tgMaui: XML namespace:
<tgMaui:MapView x:Name="MapView"
MapUnit="Meter"
DefaultOverlaysRenderSequenceType="Concurrent"
SizeChanged="MapView_OnSizeChanged">
<tgMaui:MapView.Overlays>
<tgMaui:LayerOverlay TileType="SingleTile">
...
</tgMaui:LayerOverlay>
</tgMaui:MapView.Overlays>
</tgMaui:MapView>
Blazor (ThinkGeo.UI.Blazor)¶
The MapView is a Razor component. Configuration is done in the @code block or in OnAfterRenderAsync. The API mirrors the other platforms.
GIS Server (ThinkGeo.GisServer)¶
Rather than rendering to a UI control, GIS Server exposes OGC-compliant web services (WMS, WMTS, WFS, XYZ, GeoJSON) over ASP.NET Core. You register FeatureLayer definitions as either raster (server-side styled) or vector (raw feature) endpoints:
public void Register(ThinkGeoWebServerOptions options)
{
var rasterLayers = new[]
{
new RasterLayerDefinition
{
Name = "coyoteSightings",
CreateLayer = () =>
{
var layer = new SqlServerFeatureLayer(connectionString, "frisco_coyote_sightings", "id")
{
FeatureSource = { ProjectionConverter = new ProjectionConverter(2276, 3857) }
};
layer.ZoomLevelSet.ZoomLevel01.DefaultPointStyle =
new PointStyle(PointSymbolType.Circle, 12, GeoBrushes.Black, new GeoPen(GeoColors.White, 1));
layer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
return layer;
}
}
};
options.AddWmsEndpoint(rasterLayers);
options.AddXyzEndpoint(rasterLayers);
options.AddWfsEndpoint(vectorLayers);
}
The core styling API (ZoomLevelSet, PointStyle, ProjectionConverter) is identical to client-side usage.
8. Common Patterns & Recipes¶
Minimal working map (WPF code-behind)¶
private void MapView_Loaded(object sender, RoutedEventArgs e)
{
// 1. Set the map's CRS
MapView.MapUnit = GeographyUnit.Meter;
// 2. Add a basemap overlay
MapView.Overlays.Add(new ThinkGeoCloudVectorMapsOverlay
{
ClientId = "your-client-id",
ClientSecret = "your-client-secret",
MapType = ThinkGeoCloudVectorMapsMapType.Light,
TileCache = new FileRasterTileCache(@".\cache", "basemap")
});
// 3. Create a layer and reproject it to match the map
var layer = new ShapeFileFeatureLayer(@"./Data/Hotels.shp");
layer.FeatureSource.ProjectionConverter = new ProjectionConverter(2276, 3857);
// 4. Style the layer
layer.ZoomLevelSet.ZoomLevel01.DefaultPointStyle =
new PointStyle(PointSymbolType.Circle, 12, GeoBrushes.Blue, new GeoPen(GeoBrushes.White, 2));
layer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
// 5. Add to overlay, add overlay to map
var overlay = new LayerOverlay();
overlay.Layers.Add("hotels", layer);
MapView.Overlays.Add("hotels", overlay);
// 6. Set initial view and refresh
MapView.CenterPoint = new PointShape(-10777290, 3908740);
MapView.CurrentScale = 9400;
_ = MapView.RefreshAsync();
}
In-memory features (no file needed)¶
var memoryLayer = new InMemoryFeatureLayer();
memoryLayer.InternalFeatures.Add(new Feature(new PointShape(-10777290, 3908740)));
memoryLayer.ZoomLevelSet.ZoomLevel01.DefaultPointStyle =
PointStyle.CreateSimpleCircleStyle(GeoColors.Red, 12);
memoryLayer.ZoomLevelSet.ZoomLevel01.ApplyUntilZoomLevel = ApplyUntilZoomLevel.Level20;
Query features without rendering¶
layer.Open();
try
{
var features = layer.QueryTools.GetFeaturesByColumnValue(
"Category", "Hotel",
ReturningColumnsType.AllColumns);
// features is IEnumerable<Feature>
}
finally
{
layer.Close();
}
Accessing a layer or overlay by key¶
var overlay = (LayerOverlay)MapView.Overlays["hotels"];
var hotelsLayer = (ShapeFileFeatureLayer)overlay.Layers["hotels"];
Refreshing after a style change¶
// Refresh a single overlay (fast — only that overlay re-renders)
_ = layerOverlay.RefreshAsync();
// Refresh everything
_ = MapView.RefreshAsync();
Tile caching for LayerOverlay¶
var overlay = new LayerOverlay
{
TileCache = new FileRasterTileCache(@".\cache", "myOverlayCacheKey"),
TileType = TileType.MultipleTiles
};
Cached tiles are stored on disk and reused across sessions. Invalidate by deleting the cache folder or using a different cache key.
9. Performance Checklist¶
| Concern | Recommendation |
|---|---|
| Large shapefiles | Enable ShapeFileFeatureLayer's spatial index (.idx file). Re-index with the static method: ShapeFileFeatureLayer.BuildIndex(@"./Data/file.shp") or ShapeFileFeatureLayer.BuildIndex(@"./Data/file.shp", BuildIndexMode.Rebuild) |
| Slow pan/zoom | Use TileType.MultipleTiles on LayerOverlay so tiles render concurrently and are reused |
| Basemap performance | Always attach a FileRasterTileCache to ThinkGeoCloudVectorMapsOverlay |
| Many features at low zoom | Use ClusterPointStyle or add zoom-level visibility limits via ZoomLevel min/max scale |
| Frequent data updates | Use InMemoryFeatureLayer and call RefreshAsync() only on the containing LayerOverlay |
| Multiple overlays | Set DefaultOverlaysRenderSequenceType="Concurrent" in XAML on the MapView element to render overlays in parallel (WPF/MAUI only; WinForms configures this per-overlay) |
| Heavy SQL queries | Pre-filter in your SQL WHERE clause; pass a column whitelist to GetFeaturesForDrawing |
| V14 rendering regression | If upgrading from v13, verify TileType and that no DrawingException events are silently swallowing errors |
10. Common Errors & Fixes¶
InvalidOperationException: The projection is not open¶
The ProjectionConverter must be opened before use. This happens automatically when you call layer.Open(), but if you are manually querying a FeatureSource, always call Open() first.
// Wrong
layer.FeatureSource.ProjectionConverter.ConvertToInternalProjection(rect);
// Right
layer.Open(); // opens FeatureSource + ProjectionConverter
var features = layer.QueryTools.GetAllFeatures(ReturningColumnsType.AllColumns);
layer.Close();
KeyNotFoundException on feature.ColumnValues[columnName]¶
The column was not included in the returning columns list when the features were fetched. Either request all columns or explicitly include the column:
// Request all columns
layer.QueryTools.GetAllFeatures(ReturningColumnsType.AllColumns);
// Or request specific columns
layer.QueryTools.GetAllFeatures(new[] { "Name", "Category" });
Pink/magenta tiles¶
Pink tiles indicate a rendering exception occurred inside an overlay. ThinkGeo exposes two knobs for controlling this behaviour:
LayerOverlay.ThrowingExceptionMode— set toThrowingExceptionMode.ThrowExceptionto surface exceptions, orThrowingExceptionMode.SuppressExceptionto swallow them silently (the default).LayerOverlay.ThrowingException— event raised when an exception occurs. Inspecte.Exceptionand sete.Handled = trueto prevent the runtime from re-throwing.
var layerOverlay = new LayerOverlay
{
ThrowingExceptionMode = ThrowingExceptionMode.ThrowException
};
layerOverlay.ThrowingException += (sender, e) =>
{
Debug.WriteLine($"Drawing error: {e.Exception?.Message}");
e.Handled = true; // swallow — don't rethrow
};
Per-layer, you can also control whether the layer draws a pink error tile or a custom error visual by overriding DrawExceptionCore(GeoCanvas canvas, Exception e) on your layer subclass:
protected override void DrawExceptionCore(GeoCanvas canvas, Exception e)
{
// draw a custom error indicator instead of the default pink tile
canvas.DrawArea(canvas.CurrentWorldExtent, GeoBrushes.LightOrange, DrawingLevel.LevelOne);
canvas.DrawText("Layer error", new GeoFont("Arial", 10), GeoBrushes.Red,
new[] { new ScreenPointF(canvas.Width / 2, canvas.Height / 2) }, DrawingLevel.LabelLevel);
}
Common causes: style referencing a column that was not fetched, NullReferenceException in a custom style, or an IndexOutOfRangeException in PositionStyle during line labeling.
NullReferenceException in FeatureLayer.IsDrawingNeededCore¶
This appears in MAUI and can happen when RefreshAsync is called before the map has been fully laid out. The correct pattern — confirmed in the HowDoI samples — is to wire initialization to SizeChanged in XAML and guard with a _initialized flag:
<!-- XAML -->
<tgMaui:MapView x:Name="MapView" SizeChanged="MapView_OnSizeChanged" ...>
private bool _initialized;
private async void MapView_OnSizeChanged(object sender, EventArgs e)
{
if (_initialized) return;
_initialized = true;
MapView.MapUnit = GeographyUnit.Meter;
// ... add overlays, layers, styles ...
MapView.CenterPoint = new PointShape(-10777290, 3908740);
MapView.MapScale = 200000;
await MapView.RefreshAsync();
}
Layer visible but data not appearing¶
Check in this order:
- Is
MapUnitset correctly? (GeographyUnit.Meterfor Web Mercator) - Does the layer's data CRS match what
ProjectionConverterexpects? - Is
ApplyUntilZoomLevelset? Without it, the style only applies atZoomLevel01. - Is the current scale within the style's zoom range?
- Is the layer's bounding box actually within the current map extent?
Appendix: Class Hierarchy Quick Reference¶
ThinkGeo.Core
├── MapView [UI — platform-specific subclass]
│
├── Overlay (abstract)
│ ├── LayerOverlay
│ ├── ThinkGeoCloudVectorMapsOverlay
│ ├── BingMapsOverlay
│ ├── EditOverlay
│ └── PopupOverlay
│
├── Layer (abstract)
│ ├── FeatureLayer (abstract)
│ │ ├── ShapeFileFeatureLayer
│ │ ├── SqlServerFeatureLayer
│ │ ├── InMemoryFeatureLayer
│ │ ├── GeoJsonFeatureLayer
│ │ ├── MsSqliteFeatureLayer
│ │ └── (+ many more)
│ └── (raster layers)
│ ├── GeoImageLayer
│ ├── WmsRasterLayer
│ └── GeoTiffRasterLayer
│
├── FeatureSource (abstract) [inside FeatureLayer]
│ ├── ShapeFileFeatureSource
│ ├── SqlServerFeatureSource
│ ├── InMemoryFeatureSource
│ └── (+ more)
│
├── Style (abstract)
│ ├── PointStyle
│ ├── LineStyle
│ ├── AreaStyle
│ ├── TextStyle
│ ├── ValueStyle
│ ├── ClassBreakStyle
│ ├── ClusterPointStyle
│ ├── HeatStyle
│ └── FilterStyle
│
├── ZoomLevel / ZoomLevelSet
├── Feature
├── ProjectionConverter
├── GeoCanvas (abstract) [platform-specific subclass]
├── GeoBrush / GeoPen / GeoFont
└── Shapes
├── PointShape
├── LineShape / MultilineShape
├── PolygonShape / MultipolygonShape
└── RectangleShape