Converting Local Metres to WGS84
The step where Indoor Coordinate Reference Systems meets the outside world. Everything internal is local metres; every consumer — Apple Maps, MapLibre, a tenant’s GIS — wants longitude and latitude, and four of the five ways this conversion goes wrong are invisible.
Two Steps, Not One
Local campus coordinates are metres from a surveyed origin. They are not a projection, they are an offset from one — and that distinction is the whole conversion.
Step one adds the origin’s easting and northing to produce coordinates in a real projected CRS: British National Grid, a UTM zone, a state plane. This is arithmetic, not transformation, and it requires knowing which CRS the survey was made in. That is a fact about the building recorded at survey time, and a pipeline that does not have it cannot publish.
Step two transforms those projected coordinates to EPSG:4326 with pyproj. This is a real
geodetic transformation and the library does it correctly, provided it is given the right source
CRS and told about the datum.
What Goes Wrong
The error magnitudes are the useful part. A wrong origin is 41 m and unmissable. The other three sit between 1.4 m and 4.9 m — enough to put a position in the wrong room, and not enough for anyone to notice by looking at a map.
Grid convergence is the angle between grid north and true north, which varies across a projection and is zero only on the central meridian. A building whose local frame was aligned to grid north and published as if aligned to true north is rotated by that angle; on a 200 m campus half a degree is 1.7 m at the far end.
Scale factor is the difference between distance on the projection and distance on the ground. Transverse Mercator projections are deliberately 0.9996 at the central meridian, so a 5 km campus measured on the grid is 2 m shorter than it is on the ground.
Height datum matters when publishing elevations. Orthometric height (above the geoid, what a survey gives) and ellipsoidal height (what GNSS gives) differ by tens of metres and the difference varies geographically.
Minimal Working Example
import logging
from dataclasses import dataclass
from pyproj import CRS, Transformer
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
@dataclass(frozen=True)
class CampusFrame:
"""Everything needed to place a building's local metres on the earth."""
projected_crs: str # e.g. "EPSG:27700"
origin_easting: float
origin_northing: float
rotation_deg: float = 0.0 # local frame vs. projected grid north
def local_to_wgs84(frame: CampusFrame, xy: list[tuple[float, float]]
) -> list[tuple[float, float]]:
"""Convert local campus metres to (lon, lat), applying the frame's rotation."""
import math
if not frame.projected_crs:
raise ValueError("no projected CRS recorded for this campus; cannot publish")
src = CRS.from_user_input(frame.projected_crs)
if src.is_geographic:
raise ValueError(f"{frame.projected_crs} is geographic; the origin must be projected")
tf = Transformer.from_crs(src, CRS.from_epsg(4326), always_xy=True)
th = math.radians(frame.rotation_deg)
cos_t, sin_t = math.cos(th), math.sin(th)
out: list[tuple[float, float]] = []
for x, y in xy:
# rotate the local frame onto grid north, then offset onto the projection
e = frame.origin_easting + (x * cos_t - y * sin_t)
n = frame.origin_northing + (x * sin_t + y * cos_t)
lon, lat = tf.transform(e, n)
out.append((round(lon, 8), round(lat, 8)))
logger.info("converted %d point(s) from %s to EPSG:4326", len(out), frame.projected_crs)
return out
def round_trip_error_m(frame: CampusFrame, xy: list[tuple[float, float]]) -> float:
"""Convert out and back; the residual is the transform's own error."""
import math
fwd = local_to_wgs84(frame, xy)
back = Transformer.from_crs(CRS.from_epsg(4326), CRS.from_user_input(frame.projected_crs),
always_xy=True)
worst = 0.0
for (x, y), (lon, lat) in zip(xy, fwd):
e, n = back.transform(lon, lat)
dx = (e - frame.origin_easting) - x
dy = (n - frame.origin_northing) - y
worst = max(worst, math.hypot(dx, dy))
logger.info("round-trip worst-case residual: %.4f m", worst)
return worst
The round-trip test is worth running in CI on every building. It cannot catch a wrong origin — the error cancels — but it catches a mis-specified CRS, a datum problem or a library misconfiguration, all of which produce a residual well above the few millimetres a correct transform leaves.
Common Errors & Fixes
Everything is in the Gulf of Guinea. Local metres were emitted as if they were degrees. The offset step never ran. Asserting that published longitudes and latitudes fall inside the campus’s expected bounding box catches it at build time.
The building is right and rotated. Grid convergence. If the local frame was set out to grid north, the rotation is zero and no correction is needed; if it was set out to true north or to a building line, the angle belongs in the campus frame record and in the transform.
A skybridge does not line up between two buildings. The two buildings were transformed with different origin records, or one was surveyed in a different projected CRS. This is exactly the portal-node parity problem campus CRS definition describes, seen at publication rather than at ingest.
Elevations look wrong in a consumer. Height datum. Publish orthometric heights if the consumer expects them, ellipsoidal if it does not, and record which — the difference is tens of metres and neither is detectably wrong on its own.
Integration Point
This conversion is the last step before any external publication, whether that is an IMDF archive or a vector tileset — both of which want WGS84. It runs on a copy: nothing internal is ever stored in degrees, because every geometric operation in the pipeline is defined in metres.
Its inputs come from the campus frame record established by Indoor Coordinate Reference Systems, which is why that record has to carry the projected CRS and the rotation rather than only an origin. A building whose frame record is incomplete cannot be published at all, and discovering that at survey time is far cheaper than discovering it during a submission.
Frequently Asked Questions
Can I publish without knowing the projected CRS?
No, and treating that as a hard failure saves a great deal of pain. Local metres are an offset from a projected coordinate system, and without knowing which one the offset is meaningless — there is no default that is right. Estates sometimes have the origin as a latitude and longitude instead, which is workable: transform that origin into a suitable local projected CRS once, record it, and proceed. What does not work is guessing a UTM zone, because a wrong zone puts the building hundreds of kilometres away and, worse, a neighbouring zone puts it only a few hundred metres away.
Does grid convergence really matter indoors?
It matters at campus scale and not at room scale. Half a degree of rotation is 9 millimetres over a 1-metre door and 1.7 metres at the end of a 200-metre building — so a single small building can ignore it and a campus cannot. The tell is a skybridge or tunnel that lines up at one end and not the other, which is exactly the signature of a rotation error rather than a translation one. Recording the rotation in the campus frame, even when it is zero, is what makes it reviewable.
Should the internal model ever store WGS84?
Only as a derived field, never as the working geometry. Degrees are not a metric unit: a degree of longitude is 111 km at the equator and 71 km at 50 degrees north, so buffering a wall by 0.1 in that space produces something different at every latitude and different again in the two axes. Every geometric operation in the pipeline — snapping, medial axes, distance measurement — assumes metres and is quietly wrong without them. Convert at the publishing boundary and keep the internal frame local.
Related
- Indoor Coordinate Reference Systems — where the campus frame this conversion needs is established.
- Defining Indoor CRS for Multi-Building Campuses — the survey work that produces the origin and rotation.
- Indoor Map Data Standards — the publication step that consumes WGS84 coordinates.
This page is a companion to Indoor Coordinate Reference Systems, part of the Indoor Mapping Architecture & Standards section.