ThinkGeo.com    |     Documentation    |     Premium Support

Z Value (Elevation) Support in ThinkGeo

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)

  • WKTPOINT 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 existing PointShape(x, y, z) constructor and Z property now round-trip through save/load)

  • LineShape.ZValues / RingShape.ZValues — read-only per-vertex Z list, always aligned with Vertices

  • LineShape.SetZValues(IEnumerable<double>) / RingShape.SetZValues(IEnumerable<double>) and LineShape.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.SetZValues expects exactly Vertices.Count values — closing vertex included — and the first and last Z must be equal, because they describe the same vertex (SetZ on 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 Vertices keeps 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 with SetZ(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:

  1. Set it once, at startup. The first time its value is used, it locks; changing it afterwards throws InvalidOperationException. Reading ThinkGeoSettings.PreserveZ also 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.

  2. 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 GeometryCollection and 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 = false now throws NotSupportedException instead 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)

1 Like