<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. BIM Math
  3. Master level
All lessons in this course

Lesson 40 of 44 15 minMaster

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.

Architects draw buildings square to the screen: grid line A runs left to right and the main façade faces the bottom of the sheet. The site does not care. On the survey the same building may sit half a million metres east of the map’s origin, turned 18° from north, with its ground floor 42.5 m above sea level. Georeferencing is the transform between those two descriptions, and it is a plain rigid transform from the matrices module: a turn about the vertical axis plus a move.

Two coordinate systems for one building

Project (internal) coordinatesShared (survey) coordinates
OriginA convenient point on the building, near the modelThe map projection’s origin, often hundreds of kilometres away
AxesAlong the building grid: “project north” is up on the planEast (E) and North (N) of the survey grid
ElevationUsually 0 at a reference level such as ground floorHeight above a datum such as mean sea level
Used forModelling, all geometry calculationsCoordination with other models, setting out on site, GIS

In Revit the two are linked by the project base point (a point in the model with known shared coordinates), the survey point (where the shared system is anchored), and the angle to true north. IFC stores the same information as an IfcMapConversion: eastings, northings, orthogonal height, the direction of the x axis, and a scale. Turn the angle in the widget and click the corners.

Interactive

Project north, true north and shared coordinates

ABCDEFsurvey pointPNTN
projectshared
x / E24.00512365.916
y / N10.00180222.094
z / elev0.0042.50
bearing A→B108.0° from true north
Project north is 18° east (clockwise) of true north. Corner C is (24, 10) in the project but E 512365.916, N 180222.094 on the survey grid. The building is drawn the same; only the numbers attached to it change.

Write θ for the angle from true north to project north, clockwise. A project point (x, y, z) has shared coordinates

E = E₀ + x·cos θ + y·sin θ, N = N₀ − x·sin θ + y·cos θ, elevation = Z₀ + z,

where (E₀, N₀, Z₀) are the shared coordinates of the project origin. The way back subtracts the offset and turns the other way. The building itself never changes; only the numbers describing it do.

Georef.cs
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)})";}
Output
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

It is tempting to model directly in survey coordinates so that everything “just lines up”. The robust geometry lesson showed why not: the gaps between numbers grow with their size. Graphics cards and many file formats store vertices as 32-bit floats, and at half a million metres a float has a gap of 3 cm.

Precision.cs
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)})";}
Output
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

Stored directly, the two points 1 mm apart become the same float, and every vertex of the model snaps to a 3 cm grid: curved walls look faceted and edges flicker. Subtract a local origin in double first and the float values are small, with a spacing of about half a micrometre. This is why Revit warns when geometry is more than about 20 miles from the internal origin, and why viewers such as game engines and web viewers rebase large coordinates around the camera.

Bearings

Surveyors describe directions as bearings: the angle from north, measured clockwise, from 0° to 360°. That is not the mathematician’s angle (from east, anticlockwise), so atan2 takes its arguments in the other order: bearing = atan2(ΔE, ΔN). Setting-out data is usually written in degrees, minutes and seconds.

Bearings.cs
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}\"";}
Output
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"

Round once, at the end, in the smallest unit you print. Splitting 107.99999999° into degrees, minutes and seconds separately can print 107° 59′ 60.0″.

Grid distance is not ground distance

A map projection squeezes the curved Earth onto a flat grid, so distances on the grid differ slightly from distances measured on the ground. The surveyor gives a combined scale factor (CSF) for the site, combining the projection’s scale at that location and the height above the datum. It is usually within a few parts in ten thousand of 1, which sounds negligible until you multiply it by a long building or a railway.

ScaleFactor.cs
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");
Output
   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

BIM models are drawn at true ground size. IFC’s IfcMapConversion has a Scale attribute for exactly this, and a building model sent to a GIS or compared with survey grid coordinates must apply it, or a 2 km rail model will be off by about half a metre at its far end.

Quick check

A model is placed with the angle to true north set to 25°, and every shared coordinate comes out mirrored about the north axis compared with the survey. What is the likely cause?

Quick check

Why do viewers and BIM tools keep coordinates small, near a local origin, instead of storing survey coordinates directly?

Practice

A surveyor gives you two control points in both systems: CP1 is project (5, 2) and shared (512345.373, 180220.357); CP2 is project (30, 14) and shared (512372.858, 180224.044). Work out the angle to true north θ and the shared coordinates (E₀, N₀) of the project origin, then check the result on CP3: project (12, 25) should be shared (512359.138, 180240.068). Print θ, E₀, N₀ and how far off CP3 comes out, in millimetres.

Show a hint

The direction from CP1 to CP2 is atan2(Δy, Δx) in the project and atan2(ΔN, ΔE) on the survey; θ is the first minus the second. Then E₀ = E₁ − (x₁ cos θ + y₁ sin θ) and N₀ = N₁ − (−x₁ sin θ + y₁ cos θ).

Show the solution
Solution.cs
(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");
Output
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.
0 of 44 lessons completed