Z Value (Elevation) Support in ThinkGeo
Starting with ThinkGeo.Core 15.0.0-beta088, Z values are supported end to end — and they are on by default. This feature will be part of the upcoming v15.0.0 release; you can try it today with the beta package (dotnet add package ThinkGeo.Core --version 15.0.0-beta088). If your data carries elevations (PointZ / PolylineZ / PolygonZ shapefiles, PostGIS geometries with Z, 3D data through GDAL), ThinkGeo now preserves the Z of every vertex through reading, editing, saving, reprojection and rendering. No configuration is required — and if you want to keep the previous behavior, one line disables it: set ThinkGeoSettings.PreserveZ = false at application startup (details in the compatibility section below).
This post covers what’s new, what changes for existing code, and a complete runnable sample.
What’s supported
-
Shapefiles — PointZ, MultipointZ, PolylineZ, PolygonZ: read, create and edit, including ZMin/ZMax header ranges
-
WKB — ISO types 1001–1006 in and out; PostGIS EWKB input (Z / M / SRID flags)
-
WKT —
POINT Z(...),LINESTRING Z(...), etc., in and out -
GDAL-backed sources — 3D geometries keep their Z (GeoPackage, FileGDB, …)
-
Reprojection — Z passes through unchanged
-
Rendering — Z-aware shapes draw exactly like their 2D counterparts
New APIs
Everything lives in ThinkGeo.Core — no UI package changes needed, and it works the same on WPF, WinForms, MAUI, Blazor and server-side code.
-
PointShape.HasZ— whether this point carries a Z (the existingPointShape(x, y, z)constructor andZproperty now round-trip through save/load) -
LineShape.ZValues/RingShape.ZValues— read-only per-vertex Z list, always aligned withVertices -
LineShape.SetZValues(IEnumerable<double>)/RingShape.SetZValues(IEnumerable<double>)andLineShape.SetZ(int index, double z)/RingShape.SetZ(int index, double z)— explicit Z assignment -
ThinkGeoSettings.PreserveZ— the process-wide switch (see the compatibility section below)
Quick start: reading elevations
var trailSource = new ShapeFileFeatureSource(@"Data\trails.shp");
trailSource.Open();
Feature trailFeature = trailSource.GetAllFeatures(ReturningColumnsType.AllColumns)[0];
LineShape line = ((MultilineShape)trailFeature.GetShape()).Lines[0];
var elevations = line.ZValues;
Console.WriteLine($"Elevation: {elevations.Min():0.#} m ~ {elevations.Max():0.#} m");
Writing Z data
Creating and editing Z shapefiles works with the same transaction API you already know. One thing to note: open the source with FileAccess.ReadWrite when you plan to commit.
// A shapefile needs at least one DBF column; the geometry is what matters here.
var columns = new Collection<DbfColumn> { new DbfColumn("Name", DbfColumnType.Character, 32, 0) };
ShapeFileFeatureSource.CreateShapeFile(ShapeFileType.PointZ, @"Data\summits.shp", columns, Encoding.UTF8, OverwriteMode.Overwrite);
ShapeFileFeatureSource.BuildIndexFile(@"Data\summits.shp");
var summit = new PointShape(86.9488, 27.9892, 2172); // x, y, z
var summitSource = new ShapeFileFeatureSource(@"Data\summits.shp", FileAccess.ReadWrite);
summitSource.Open();
summitSource.BeginTransaction();
summitSource.AddFeature(new Feature(summit));
summitSource.CommitTransaction();
var summitBack = (PointShape)summitSource.GetAllFeatures(ReturningColumnsType.AllColumns)[0].GetShape();
Console.WriteLine($"Summit read: HasZ={summitBack.HasZ}, Z={summitBack.Z:0.#} m at ({summitBack.X:0.####}, {summitBack.Y:0.####})");
The read-back prints HasZ=True, Z=2172 m, and the file opens as a 3D point layer in QGIS.
Building Z geometries in code
ZValues always stays aligned with Vertices: you assign one Z per vertex explicitly.
var line = new LineShape(new[]
{
new Vertex(0, 0),
new Vertex(10, 10),
new Vertex(20, 5)
});
line.SetZValues(new[] { 100.0, 125.0, 110.0 }); // one Z per vertex
Console.WriteLine(line.GetWellKnownText()); // LINESTRING Z(0 0 100,10 10 125,20 5 110)
Two rules keep that alignment safe:
-
Rings include the closing vertex.
RingShape.SetZValuesexpects exactlyVertices.Countvalues — closing vertex included — and the first and last Z must be equal, because they describe the same vertex (SetZon the first or the last vertex updates both):
// 5 Z values for 5 vertices, including the closing one; first and last must match.
ring.SetZValues(new[] { 1.0, 2.0, 3.0, 4.0, 1.0 });
-
Editing
Verticeskeeps Z aligned automatically. Inserting a vertex between two others interpolates its Z from the neighbors, inserting at the head or tail copies the nearest endpoint’s Z, and removing a vertex removes its Z with it. The derived value can always be overwritten withSetZ(index, z).
Compatibility: the PreserveZ switch
Z support changes what some existing APIs return for Z-bearing data. For example, PointShape(x, y, z).GetWellKnownText() now returns POINT Z(x y z) instead of POINT(x y), and the WKB written for it uses ISO type 1001 instead of 1.
If your data is pure 2D, nothing changes. 2D geometries never enter the Z code paths at all, so 2D shapefiles, 2D WKB/WKT and every existing 2D workflow produce byte-identical output. Our full regression suite passes unchanged in both modes.
If you do handle Z data and need the old behavior — for example your code compares WKT strings, persists WKB bytes, or exchanges WKB with applications running older ThinkGeo versions — set the switch off once at application startup:
// First line of your app, before any layer or FeatureSource is opened.
ThinkGeoSettings.PreserveZ = false;
Two rules about this switch:
-
Set it once, at startup. The first time its value is used, it locks; changing it afterwards throws
InvalidOperationException. ReadingThinkGeoSettings.PreserveZalso counts as using it — set the switch before any layer or FeatureSource is opened and before any code (yours or a library’s) queries the property. Re-assigning the same value is always allowed. -
It is process-wide, not per-layer — Z either round-trips everywhere or nowhere, so different components can never disagree about what a geometry’s bytes mean.
With the switch off you get the exact legacy behavior, plus a one-time warning per file in your Trace/debug output when a Z-bearing shapefile is read (so silently dropped elevations are no longer invisible).
Known limitations
-
This is not 3D rendering. Z values are preserved as per-vertex data; they do not change 2D drawing, draw order or hit testing, and they do not by themselves provide 3D/terrain visualization.
-
Reprojection does not transform Z. Values pass through unchanged; there is no vertical datum conversion.
-
M values are not modeled. ZM input is accepted — Z is kept, M is dropped. M-type shapefiles (PointM, etc.) read as 2D and remain non-editable.
-
Geometry operations stay 2D. Buffer, union, intersection and similar results carry no Z.
-
Z inside
GeometryCollectionand GeoJSON third coordinates are not supported yet. -
A multi-geometry mixing 2D and Z children is rejected with a clear exception rather than silently flattened.
-
Editing a Z or M shapefile with
PreserveZ = falsenow throwsNotSupportedExceptioninstead of silently corrupting the file (this is a bug fix — previous versions could write 2D records into a Z file).
Try it
ZValueQuickStart.zip (3.4 KB)