Survey points, true north and georeferencing
How a BIM model sits on the real site: project and shared coordinates, the angle to true north, why models stay near their own origin, bearings, and the grid scale factor.
Two coordinate systems for one building
| Project (internal) coordinates | Shared (survey) coordinates | |
|---|---|---|
var site = new Georef(E0: 512_340, N0: 180_220, Z0: 42.5, AngleDeg: 18); var corners = new (string Name, V3 P)[]{ ("A", new(0, 0, 0)), ("B", new(24, 0, 0)), ("C", new(24, 10, 0)), ("E roof", new(10, 18, 3.5))};foreach (var (name, p) in corners){ var s = site.ToShared(p); Console.WriteLine($"{name,-6} project {p,-14} E {s.X:0.000} N {s.Y:0.000} elev {s.Z:0.000}");}Console.WriteLine($"C there and back: {site.ToProject(site.ToShared(new V3(24, 10, 0)))}"); readonly record struct Georef(double E0, double N0, double Z0, double AngleDeg){ // Project north is AngleDeg clockwise from true (grid) north. public V3 ToShared(V3 p) { double t = AngleDeg * Math.PI / 180, c = Math.Cos(t), s = Math.Sin(t); return new(E0 + p.X * c + p.Y * s, N0 - p.X * s + p.Y * c, Z0 + p.Z); } public V3 ToProject(V3 g) { double t = AngleDeg * Math.PI / 180, c = Math.Cos(t), s = Math.Sin(t); double e = g.X - E0, n = g.Y - N0; return new(e * c - n * s, e * s + n * c, g.Z - Z0); }} readonly record struct V3(double X, double Y, double Z){ public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z); public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z); public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k); public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z; public static V3 Cross(V3 a, V3 b) => new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X); public double Length => Math.Sqrt(Dot(this, this)); public V3 Normalized() => this * (1 / Length); static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###"); public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}A project (0, 0, 0) E 512340.000 N 180220.000 elev 42.500 B project (24, 0, 0) E 512362.825 N 180212.584 elev 42.500 C project (24, 10, 0) E 512365.916 N 180222.094 elev 42.500 E roof project (10, 18, 3.5) E 512355.073 N 180234.029 elev 46.000 C there and back: (24, 10, 0)
Why the model stays near its own origin
var p = new V3(512_345.678, 180_223.456, 47.25); // a point in shared coordinatesvar q = new V3(512_345.679, 180_223.456, 47.25); // 1 mm east of it float px = (float)p.X, qx = (float)q.X;Console.WriteLine($"stored as float: {px:0.000} and {qx:0.000}, gap {qx - px:0.000} m"); var origin = new V3(512_340, 180_220, 42.5); // a local origin, kept in doublefloat lp = (float)(p.X - origin.X), lq = (float)(q.X - origin.X);Console.WriteLine($"relative to the origin: {lp:0.000000} and {lq:0.000000}, gap {lq - lp:0.000000} m");Console.WriteLine($"float spacing at 512345: {MathF.BitIncrement(px) - px} m, at 5.678: {MathF.BitIncrement(lp) - lp:0.0E0} m"); readonly record struct V3(double X, double Y, double Z){ public static V3 operator +(V3 a, V3 b) => new(a.X + b.X, a.Y + b.Y, a.Z + b.Z); public static V3 operator -(V3 a, V3 b) => new(a.X - b.X, a.Y - b.Y, a.Z - b.Z); public static V3 operator *(V3 v, double k) => new(v.X * k, v.Y * k, v.Z * k); public static double Dot(V3 a, V3 b) => a.X * b.X + a.Y * b.Y + a.Z * b.Z; public static V3 Cross(V3 a, V3 b) => new(a.Y * b.Z - a.Z * b.Y, a.Z * b.X - a.X * b.Z, a.X * b.Y - a.Y * b.X); public double Length => Math.Sqrt(Dot(this, this)); public V3 Normalized() => this * (1 / Length); static string F(double v) => Math.Abs(v) < 5e-4 ? "0" : v.ToString("0.###"); public override string ToString() => $"({F(X)}, {F(Y)}, {F(Z)})";}stored as float: 512345.700 and 512345.700, gap 0.000 m relative to the origin: 5.678000 and 5.679000, gap 0.001000 m float spacing at 512345: 0.03125 m, at 5.678: 4.8E-7 m
Bearings
double t = 18 * Math.PI / 180; // project north is 18 deg east of north var lines = new (string Name, double X, double Y)[]{ ("A to B, along project x", 24, 0), ("B to C, along project y", 0, 10), ("A to D, a diagonal", 10, 10)};foreach (var (name, x, y) in lines){ double e = x * Math.Cos(t) + y * Math.Sin(t), n = -x * Math.Sin(t) + y * Math.Cos(t); double bearing = Math.Atan2(e, n) * 180 / Math.PI; // from north, clockwise if (bearing < 0) bearing += 360; Console.WriteLine($"{name,-24} {Math.Sqrt(e * e + n * n),7:0.000} m at {bearing,8:0.0000} deg = {Dms(bearing)}");} static string Dms(double deg){ long tenths = (long)Math.Round(deg * 36000); // tenths of a second, rounded once return $"{tenths / 36000}d {tenths / 600 % 60:00}' {tenths % 600 / 10.0:00.0}\"";}A to B, along project x 24.000 m at 108.0000 deg = 108d 00' 00.0" B to C, along project y 10.000 m at 18.0000 deg = 18d 00' 00.0" A to D, a diagonal 14.142 m at 63.0000 deg = 63d 00' 00.0"
Grid distance is not ground distance
const double Csf = 0.99974; // combined scale factor from the site survey foreach (double ground in new[] { 24.0, 100, 2000 }){ double grid = ground * Csf; Console.WriteLine($"{ground,5:0} m on the ground = {grid,9:0.000} m on the grid, {(ground - grid) * 1000,4:0} mm shorter");}Console.WriteLine($"control points 1500 m apart on the grid are {1500 / Csf:0.000} m apart on site");24 m on the ground = 23.994 m on the grid, 6 mm shorter 100 m on the ground = 99.974 m on the grid, 26 mm shorter 2000 m on the ground = 1999.480 m on the grid, 520 mm shorter control points 1500 m apart on the grid are 1500.390 m apart on site
Show a hint
Show the solution
(double x, double y, double e, double n) cp1 = (5, 2, 512345.373, 180220.357);(double x, double y, double e, double n) cp2 = (30, 14, 512372.858, 180224.044); double angProject = Math.Atan2(cp2.y - cp1.y, cp2.x - cp1.x);double angShared = Math.Atan2(cp2.n - cp1.n, cp2.e - cp1.e);double t = angProject - angShared, c = Math.Cos(t), s = Math.Sin(t); double e0 = cp1.e - (cp1.x * c + cp1.y * s);double n0 = cp1.n - (-cp1.x * s + cp1.y * c);Console.WriteLine($"angle to true north: {t * 180 / Math.PI:0.0000} deg");Console.WriteLine($"project origin: E {e0:0.000}, N {n0:0.000}"); double x3 = 12, y3 = 25;double e3 = e0 + x3 * c + y3 * s, n3 = n0 - x3 * s + y3 * c;double off = Math.Sqrt((e3 - 512359.138) * (e3 - 512359.138) + (n3 - 180240.068) * (n3 - 180240.068));Console.WriteLine($"CP3 computed: E {e3:0.000}, N {n3:0.000}, off by {off * 1000:0.0} mm");angle to true north: 18.0006 deg project origin: E 512340.000, N 180220.000 CP3 computed: E 512359.138, N 180240.068, off by 0.0 mm
Remember
Project coordinates are for modelling; shared (survey) coordinates place the model on the site. Georeferencing is a rigid transform: E = E₀ + x cos θ + y sin θ, N = N₀ − x sin θ + y cos θ, elevation = Z₀ + z. Angle conventions differ between programs, so check every transform with a known point. Keep geometry near a local origin; floats at survey coordinates have centimetre-sized gaps. Bearings are measured clockwise from north: atan2(ΔE, ΔN). Ground and grid distances differ by the combined scale factor.