Skip to content

DHI.Spatial.Projections — Internal Developer Guide

A tiny, focused utility for reprojecting DHI.Spatial geometries between coordinate reference systems (CRS) using DotSpatial.Projections.

  • Namespace: DHI.Spatial.Projections
  • Primary type: CoordinateTransformer (static)
  • Depends on: DHI.Spatial (our geometry model), DotSpatial.Projections

Projection is done vertex-by-vertex. Topology is not guaranteed to remain valid after transformation (e.g., self-intersections may appear).


What it does

  • Reprojects individual geometries: Point, MultiPoint, LineString, MultiLineString, Polygon, MultiPolygon.
  • Reprojects collections of geometries.
  • Accepts CRS as either:
    • EPSG code strings (e.g., "4326", "3857"), or
    • DHI CoordinateReferenceSystem objects with Type = "name" and a "name" property like "EPSG:4326".

Public API

// Project a single geometry by EPSG codes
public static IGeometry Project(IGeometry geometry, string inSrs, string outSrs);

// Project a collection by EPSG codes
public static IEnumerable<IGeometry> Project(IEnumerable<IGeometry> geometries, string inSrs, string outSrs);

// Project a single geometry using its own CRS to a target CRS
public static IGeometry Project(IGeometry geometry, CoordinateReferenceSystem outCrs);

// Project a collection of geometries using each geometry’s own CRS to a target CRS
public static IEnumerable<IGeometry> Project(IEnumerable<IGeometry> geometries, CoordinateReferenceSystem outCrs);

Behavior & expectations

  • Axis order: DotSpatial uses X = longitude, Y = latitude for geographic CRS such as EPSG:4326. Provide coordinates accordingly.
  • Z handling: If a coordinate has Z:
    • We pass Z through to DotSpatial (as an array). Many projections do not alter Z, so expect Z to be preserved. If original Z is null, we pass 0 and do not emit a Z in the result for that vertex.
  • CRS on result:
    • Project(geometry, inSrs, outSrs): does not set geometry.CRS on the result (caller may set it).
    • Project(geometry, CoordinateReferenceSystem outCrs): does set result.CRS = outCrs.
  • Collections: Collection overloads simply map each geometry through the corresponding single-geometry method.

Usage examples

1) EPSG -> EPSG (single geometry)

using DHI.Spatial;
using DHI.Spatial.Projections;

var p4326 = new Point(new Position(12.0, 55.0)); // lon, lat
var p3857 = CoordinateTransformer.Project(p4326, "4326", "3857");

// Optionally record the CRS on the result yourself:
p3857.CRS = new CoordinateReferenceSystem { Type = "name" };
p3857.CRS.Properties["name"] = "EPSG:3857";

2) Geometry with CRS -> target CRS

var line = new LineString();
line.Coordinates.Add(new Position(12.0,55.0));
line.Coordinates.Add(new Position(12.5,55.1));

line.CRS = new CoordinateReferenceSystem { Type = "name" };
line.CRS.Properties["name"] = "EPSG:4326";

var outCrs = new CoordinateReferenceSystem { Type = "name" };
outCrs.Properties["name"] = "EPSG:3857";

var line3857 = CoordinateTransformer.Project(line, outCrs);
// line3857.CRS == outCrs

3) Batch reproject

IEnumerable<IGeometry> geoms = GetGeometries();  // no CRS on geometries
var outGeoms = CoordinateTransformer.Project(geoms, "4326", "3857");

or when each geometry has a CRS:

var outCrs = new CoordinateReferenceSystem { Type = "name" };
outCrs.Properties["name"] = "EPSG:3857";
var outGeoms = CoordinateTransformer.Project(geoms, outCrs);

Supported geometry types

  • Point
  • MultiPoint
  • LineString
  • MultiLineString
  • Polygon (outer ring + holes, if present)
  • MultiPolygon (with holes)
  • GeometryCollection (project its members individually)

CRS parsing rules

When using the CRS object overloads:

  • We require CoordinateReferenceSystem.Type == "name".
  • We require a "name" property containing something like "EPSG:4326" or a URN ending with the EPSG code.
  • The code uses only the last colon-separated token, e.g.:
    • "EPSG:4326" -> "4326"
    • "urn:ogc:def:crs:EPSG::4326" -> "4326"

If these conditions aren’t met, we throw ArgumentException.


Errors & exceptions

  • If inSrs or outSrs is not an integer -> ArgumentException (“invalid format”).
  • If EPSG code is not registered with DotSpatial -> ArgumentException (“not registered”).
  • If a geometry lacks CRS in the CRS-object overload -> ArgumentException.
  • Unknown geometry types return null (and skip the throw). Prefer to guard your inputs.

Topology & quality notes

  • No topology guarantees. We reproject vertices and rebuild shapes:
    • Polygons might become invalid (self-intersection) after projection.
    • Ring orientation is preserved as-is; we don’t enforce closure or orientation corrections.
  • Densification: If projecting long segments in geographic CRS (e.g., near the dateline), consider densifying before projecting.
  • Precision: Reprojection is performed in double precision; round-off can still alter shape characteristics slightly.

Performance characteristics

  • Each projection call flattens coordinates to XY/Z arrays and invokes Reproject.ReprojectPoints once per geometry. This is efficient for medium/large geometries.
  • Collection overloads recompute ProjectionInfo per geometry (because they call the single-geometry method). For very large batches with the same SRS pair, consider extracting the code to reuse ProjectionInfo (potential improvement).
  • Memory: Arrays are sized to the total vertex count of the geometry. For very large MultiPolygons, this can be substantial.

Best practices

  • Axis order: Always pass (lon, lat) for 4326. If your source is (lat, lon), swap before projecting.
  • CRS alias “900913”: Historically used for Web Mercator; EPSG:3857 is the canonical code. Depending on DotSpatial version, "900913" may or may not resolve. Prefer "3857".
  • Z values: We pass Z through. Most horizontal CRS transforms won’t change Z; if you rely on true vertical transformations, handle Z separately.
  • Result CRS: If you use the string overloads, remember to set CRS on the result yourself (or switch to the CoordinateReferenceSystem overload).

Internals (how it’s built)

  • For each supported geometry kind we:
    1. Flatten coordinates into double[] xy and double[] z.
    2. Call Reproject.ReprojectPoints(xy, z, pStart, pEnd, 0, count).
    3. Reconstruct a new geometry, preserving the presence/absence of Z on each vertex.

This design keeps the code straightforward and maps cleanly onto DotSpatial’s API.