A .NET Standard 2.0 library providing coordinate transformations between geographic coordinates (Latitude, Longitude, Altitude) and local Cartesian coordinate systems.
$ dotnet add package Cooney.Geospatial[!WARNING] This library is entirely vibe coded. No assumptions should be made about its correctness for any purpose whatsoever.
A .NET Standard 2.0 library providing coordinate transformations between geographic coordinates (Latitude, Longitude, Altitude) and local Cartesian coordinate systems. This library is based on the WGS84 ellipsoid model and uses ECEF (Earth-Centered, Earth-Fixed) coordinates as an intermediate representation.
This implementation is designed to be framework-agnostic and can be used in Unity, non-Unity applications, and any .NET Standard 2.0 compatible environment.
Latitude, Longitude, Altitude
Geographic coordinates represent positions on Earth's surface using angular measurements and height.
Earth-Centered, Earth-Fixed Cartesian Coordinates
ECEF coordinates are Cartesian (X, Y, Z) coordinates in meters from Earth's center. This system rotates with the Earth.
East-North-Up Local Tangent Plane
ENU coordinates are relative to a local origin point on Earth's surface, providing a convenient local Cartesian frame.
When integrating with Unity or other game engines:
The library provides conversion methods to transform between ENU and Unity-style coordinate systems.
The World Geodetic System 1984 (WGS84) is the standard coordinate system used by GPS and most mapping applications.
Semi-major axis (a): 6,378,137.0 meters
Flattening (f): 1/298.257223563
Semi-minor axis (b): 6,356,752.314 meters
Eccentricity² (e²): 0.00669437999014
The Earth is modeled as an oblate ellipsoid (slightly flattened sphere), not a perfect sphere. This accounts for the equatorial bulge caused by Earth's rotation.
using Cooney.Geospatial;
// Create a geographic coordinate (Sydney Harbour Bridge)
var geographic = new GeographicCoordinates(
latitude: -33.852222,
longitude: 151.210556,
altitude: 0
);
// Convert to ECEF
EcefCoordinates ecef = Wgs84Ellipsoid.GeographicToEcef(geographic);
Console.WriteLine(ecef); // ECEF(X: -4648237.123m, Y: 2560392.789m, Z: -3526423.456m)
// Convert back to geographic
GeographicCoordinates geo = Wgs84Ellipsoid.EcefToGeographic(ecef);
Console.WriteLine(geo); // Lat: -33.852222°, Lon: 151.210556°, Alt: 0.00m
using Cooney.Geospatial;
using System.Numerics;
// Set up origin (Sydney CBD)
var origin = new GeographicCoordinates(-33.8688, 151.2093, 0);
EcefCoordinates originEcef = Wgs84Ellipsoid.GeographicToEcef(origin);
// Calculate transformation matrix
var ecefToEnuMatrix = CoordinateTransformations.CalculateEcefToEnuMatrix(
origin.Latitude,
origin.Longitude
);
// Convert Sydney Tower to local ENU coordinates
var sydneyTower = new GeographicCoordinates(-33.8704, 151.2093, 309);
EcefCoordinates sydneyTowerEcef = Wgs84Ellipsoid.GeographicToEcef(sydneyTower);
Double3 enuPosition = CoordinateTransformations.EcefToEnu(
sydneyTowerEcef,
originEcef,
ecefToEnuMatrix
);
Console.WriteLine($"Sydney Tower relative position: E={enuPosition.x:F2}m, N={enuPosition.y:F2}m, U={enuPosition.z:F2}m");
using Cooney.Geospatial;
using System.Numerics;
// Set up origin and matrices
var origin = new GeographicCoordinates(-33.8688, 151.2093, 0);
EcefCoordinates originEcef = Wgs84Ellipsoid.GeographicToEcef(origin);
var ecefToEnuMatrix = CoordinateTransformations.CalculateEcefToEnuMatrix(
origin.Latitude,
origin.Longitude
);
// Convert a location to Unity coordinates
var location = new GeographicCoordinates(-33.8704, 151.2093, 309);
Vector3 unityPosition = CoordinateTransformations.EcefToUnity(
Wgs84Ellipsoid.GeographicToEcef(location),
originEcef,
ecefToEnuMatrix
);
Console.WriteLine($"Unity position: X={unityPosition.X}, Y={unityPosition.Y}, Z={unityPosition.Z}");
// Convert back from Unity to geographic
var enuToEcefMatrix = CoordinateTransformations.CalculateEnuToEcefMatrix(
origin.Latitude,
origin.Longitude
);
EcefCoordinates ecef = CoordinateTransformations.UnityToEcef(
unityPosition,
originEcef,
enuToEcefMatrix
);
GeographicCoordinates geoFromUnity = Wgs84Ellipsoid.EcefToGeographic(ecef);
Represents a position in latitude, longitude, and altitude.
public struct GeographicCoordinates
{
public double Latitude { get; set; } // Degrees, -90 to +90
public double Longitude { get; set; } // Degrees, -180 to +180
public double Altitude { get; set; } // Meters above WGS84 ellipsoid
public GeographicCoordinates(double latitude, double longitude, double altitude = 0);
public bool IsValid(); // Check if coordinates are within valid ranges
public void Normalize(); // Normalize longitude wrapping and clamp latitude
public string ToString(); // "Lat: -33.852222°, Lon: 151.210556°, Alt: 0.00m"
public string ToDetailedString(); // "33.852222°S, 151.210556°E, 0.00m"
}
Methods:
IsValid(): Returns true if latitude is in [-90, 90], longitude is in [-180, 180], and values are not NaN or InfinityNormalize(): Clamps latitude to valid range and wraps longitude to [-180, 180]ToString(): Returns formatted string with decimal degreesToDetailedString(): Returns formatted string with cardinal directions (N/S/E/W)Represents a position in Earth-Centered, Earth-Fixed coordinates.
public struct EcefCoordinates
{
public double X { get; set; } // Meters
public double Y { get; set; } // Meters
public double Z { get; set; } // Meters
public EcefCoordinates(double x, double y, double z);
public Double3 ToDouble3(); // Convert to Double3
public static EcefCoordinates FromDouble3(Double3); // Create from Double3
public double Magnitude(); // Distance from Earth's center
public EcefCoordinates Normalised(); // Unit vector
// Operators: +, -, *, /
}
Methods:
Magnitude(): Returns the distance from Earth's center (typically ~6.37 million meters)Normalised(): Returns a unit vector in the same directionStatic class providing WGS84 constants and conversion methods.
public static class Wgs84Ellipsoid
{
// Constants
public const double SemiMajorAxis = 6378137.0; // meters
public const double Flattening = 1.0 / 298.257223563;
public const double SemiMinorAxis; // 6,356,752.314m
public const double EccentricitySquared; // 0.00669437999014
public const double SecondEccentricitySquared;
// Conversion methods
public static EcefCoordinates GeographicToEcef(GeographicCoordinates geographic);
public static GeographicCoordinates EcefToGeographic(EcefCoordinates ecef);
// Utility methods
public static double CalculateRadiusOfCurvature(double latitudeInDegrees);
public static double CalculateMeridionalRadius(double latitudeInDegrees);
}
Conversion Methods:
GeographicToEcef(): Converts LLA to ECEF using standard WGS84 formulasEcefToGeographic(): Converts ECEF to LLA using Bowring method (closed-form solution)Utility Methods:
CalculateRadiusOfCurvature(): Returns the radius of curvature in the prime vertical (N)CalculateMeridionalRadius(): Returns the radius of curvature in the meridian (M)Static class providing transformations between ECEF, ENU, and Unity coordinate systems.
public static class CoordinateTransformations
{
// Matrix calculations
public static Double3x3 CalculateEcefToEnuMatrix(double latitudeInDegrees, double longitudeInDegrees);
public static Double3x3 CalculateEnuToEcefMatrix(double latitudeInDegrees, double longitudeInDegrees);
// ECEF ° ENU conversions
public static Double3 EcefToEnu(EcefCoordinates ecef, EcefCoordinates origin, Double3x3 ecefToEnuMatrix);
public static EcefCoordinates EnuToEcef(Double3 enu, EcefCoordinates origin, Double3x3 enuToEcefMatrix);
// ENU ° Unity conversions
public static Vector3 EnuToUnity(Double3 enu);
public static Double3 UnityToEnu(Vector3 unity);
// Direct ECEF ° Unity conversions
public static Vector3 EcefToUnity(EcefCoordinates ecef, EcefCoordinates origin, Double3x3 ecefToEnuMatrix);
public static EcefCoordinates UnityToEcef(Vector3 unity, EcefCoordinates origin, Double3x3 enuToEcefMatrix);
}
Matrix Methods:
CalculateEcefToEnuMatrix(): Creates rotation matrix from ECEF to ENU at given originCalculateEnuToEcefMatrix(): Creates rotation matrix from ENU to ECEF (transpose of above)Coordinate Conversions:
public struct Double3
{
public double x, y, z;
public Double3(double x, double y, double z);
}
public struct Double3x3
{
public Double3 c0, c1, c2; // Column vectors
public Double3x3(Double3 c0, Double3 c1, Double3 c2);
}
public static class Maths
{
public const double DegreesToRadians = Math.PI / 180.0;
public const double RadiansToDegrees = 180.0 / Math.PI;
public static Double3 Mul(Double3x3 m, Double3 v); // Matrix-vector multiplication
public static Double3x3 Transpose(Double3x3 m); // Matrix transpose
}
using Cooney.Geospatial;
var sydney = new GeographicCoordinates(-33.8688, 151.2093, 0);
var melbourne = new GeographicCoordinates(-37.8136, 144.9631, 0);
// Convert to ECEF
var sydneyEcef = Wgs84Ellipsoid.GeographicToEcef(sydney);
var melbourneEcef = Wgs84Ellipsoid.GeographicToEcef(melbourne);
// Calculate straight-line distance through Earth
EcefCoordinates difference = melbourneEcef - sydneyEcef;
double distance = difference.Magnitude();
Console.WriteLine($"Sydney to Melbourne: {distance / 1000.0:F1} km");
// Note: This is straight-line distance, not great-circle distance
using Cooney.Geospatial;
using System.Numerics;
using System.Collections.Generic;
public class LocalCoordinateSystem
{
private GeographicCoordinates _origin;
private EcefCoordinates _originEcef;
private Double3x3 _ecefToEnuMatrix;
private Double3x3 _enuToEcefMatrix;
public LocalCoordinateSystem(GeographicCoordinates origin)
{
_origin = origin;
_originEcef = Wgs84Ellipsoid.GeographicToEcef(origin);
_ecefToEnuMatrix = CoordinateTransformations.CalculateEcefToEnuMatrix(
origin.Latitude,
origin.Longitude
);
_enuToEcefMatrix = CoordinateTransformations.CalculateEnuToEcefMatrix(
origin.Latitude,
origin.Longitude
);
}
public Vector3 GeographicToLocal(GeographicCoordinates geographic)
{
EcefCoordinates ecef = Wgs84Ellipsoid.GeographicToEcef(geographic);
return CoordinateTransformations.EcefToUnity(ecef, _originEcef, _ecefToEnuMatrix);
}
public GeographicCoordinates LocalToGeographic(Vector3 localPosition)
{
EcefCoordinates ecef = CoordinateTransformations.UnityToEcef(
localPosition,
_originEcef,
_enuToEcefMatrix
);
return Wgs84Ellipsoid.EcefToGeographic(ecef);
}
}
// Usage
var system = new LocalCoordinateSystem(new GeographicCoordinates(-33.8688, 151.2093, 0));
var sydneyTower = new GeographicCoordinates(-33.8704, 151.2093, 309);
Vector3 localPos = system.GeographicToLocal(sydneyTower);
Console.WriteLine($"Local position: {localPos}");
GeographicCoordinates recovered = system.LocalToGeographic(localPos);
Console.WriteLine($"Recovered: {recovered}");
using Cooney.Geospatial;
var coords = new GeographicCoordinates(91.5, 185.0, 100);
Console.WriteLine($"Is valid: {coords.IsValid()}"); // False (lat > 90, lon > 180)
coords.Normalize();
Console.WriteLine($"After normalization: {coords}");
// Lat: 90.000000°, Lon: -175.000000°, Alt: 100.00m
Console.WriteLine($"Is valid: {coords.IsValid()}"); // True
using Cooney.Geospatial;
// Get ECEF coordinates for two points
var point1 = new GeographicCoordinates(0, 0, 0); // Equator, Prime Meridian
var point2 = new GeographicCoordinates(90, 0, 0); // North Pole
var ecef1 = Wgs84Ellipsoid.GeographicToEcef(point1);
var ecef2 = Wgs84Ellipsoid.GeographicToEcef(point2);
Console.WriteLine($"Equator/Prime Meridian: {ecef1}");
Console.WriteLine($"North Pole: {ecef2}");
// Vector operations
EcefCoordinates midpoint = (ecef1 + ecef2) / 2.0;
Console.WriteLine($"Midpoint: {midpoint}");
// Distance from Earth's center
Console.WriteLine($"Equator radius: {ecef1.Magnitude():F1}m");
Console.WriteLine($"Polar radius: {ecef2.Magnitude():F1}m");
Given latitude φ, longitude λ, and altitude h:
N(φ) = a / √(1 - e² · sin²(φ))
X = (N(φ) + h) · cos(φ) · cos(λ)
Y = (N(φ) + h) · cos(φ) · sin(λ)
Z = (N(φ) · (1 - e²) + h) · sin(φ)
Where:
a = WGS84 semi-major axis (6,378,137.0 m)
e² = WGS84 eccentricity squared (0.00669437999014)
N(φ) = radius of curvature in the prime vertical
This is a closed-form solution that's efficient and accurate:
λ = atan2(Y, X)
p = √(X² + Y²)
θ = atan2(Z · a, p · b)
φ = atan2(Z + e'² · b · sin³(θ), p - e² · a · cos³(θ))
N = a / √(1 - e² · sin²(φ))
h = p / cos(φ) - N
Where:
b = WGS84 semi-minor axis
e'² = second eccentricity squared
θ = parametric latitude (intermediate value)
At a given origin (φ₀, λ₀), the rotation matrix from ECEF to ENU is:
East = [-sin(λ₀), cos(λ₀), 0]
North = [-sin(φ₀)·cos(λ₀), -sin(φ₀)·sin(λ₀), cos(φ₀)]
Up = [ cos(φ₀)·cos(λ₀), cos(φ₀)·sin(λ₀), sin(φ₀)]
R = [East; North; Up]
The inverse transformation (ENU to ECEF) is the transpose: R^T = R^(-1)
ENU (Right-handed, Z-up): [East, North, Up]
Unity (Left-handed, Y-up): [X=East, Y=Up, Z=North]
Transformation:
Unity.X = ENU.x (East)
Unity.Y = ENU.z (Up)
Unity.Z = ENU.y (North)
When using an origin-based local coordinate system:
The precision limit comes from float's ~7 decimal digits of precision. At 1000km (1,000,000m), float precision is approximately 1,000,000 / 10^7 H 0.1m.
This library is designed to integrate seamlessly with Unity projects:
using UnityEngine;
using Cooney.Geospatial;
public class GeoreferencingManager : MonoBehaviour
{
private LocalCoordinateSystem _coordinateSystem;
void Start()
{
// Set origin to Sydney CBD
var origin = new GeographicCoordinates(-33.8688, 151.2093, 0);
_coordinateSystem = new LocalCoordinateSystem(origin);
}
public void PlaceObjectAtGeographicLocation(GameObject obj, GeographicCoordinates location)
{
Vector3 localPos = _coordinateSystem.GeographicToLocal(location);
obj.transform.position = new UnityEngine.Vector3(localPos.X, localPos.Y, localPos.Z);
}
public GeographicCoordinates GetGeographicLocation(GameObject obj)
{
var pos = obj.transform.position;
return _coordinateSystem.LocalToGeographic(new System.Numerics.Vector3(pos.x, pos.y, pos.z));
}
}
Note: You may need to convert between UnityEngine.Vector3 and System.Numerics.Vector3.
ecefToEnuMatrix and enuToEcefMatrix once per originPotential features for future releases: