Skip to content

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

  1. Package & Namespace Structure
  2. The Core Object Model
  3. The Five-Layer Stack
  4. The Rendering Pipeline
  5. Coordinate Systems & Projections
  6. Styling System
  7. Platform Targets
  8. Common Patterns & Recipes
  9. Performance Checklist
  10. 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.Core and 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 more Layer objects. This is what you use for your own data.
  • Background overlaysThinkGeoCloudVectorMapsOverlay, 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 include ShapeFileFeatureLayer, SqlServerFeatureLayer, MsSqliteFeatureLayer, InMemoryFeatureLayer, GeoJsonFeatureLayer, and many more.
  • Raster layersGeoImageLayer, 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:

  • GeoCanvas is the drawing surface abstraction. It wraps the platform's native graphics API (GDI+ on WinForms, Skia on MAUI, etc.). You never create a GeoCanvas directly; it is passed into DrawCore.
  • ZoomLevel controls which styles are active at each scale. ZoomLevel01 is the most zoomed-in (highest scale denominator), ZoomLevel20 is zoomed out. ApplyUntilZoomLevel cascades a style across a range.
  • Labels are accumulated in a shared labelsInAllLayers collection 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 to ThrowingExceptionMode.ThrowException to surface exceptions, or ThrowingExceptionMode.SuppressException to swallow them silently (the default).
  • LayerOverlay.ThrowingException — event raised when an exception occurs. Inspect e.Exception and set e.Handled = true to 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:

  1. Is MapUnit set correctly? (GeographyUnit.Meter for Web Mercator)
  2. Does the layer's data CRS match what ProjectionConverter expects?
  3. Is ApplyUntilZoomLevel set? Without it, the style only applies at ZoomLevel01.
  4. Is the current scale within the style's zoom range?
  5. 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