Skip to content

Coordinate reference matrix

This matrix accounts for every coordinate-bearing value in the public client: the models, the JSON and protobuf codecs, the operator application, and the examples and fixtures. It records what each value means, not how it is transported. Location.source is a free-text producer label with no coordinate, axis, unit, or datum content, so it is outside this matrix and excluded from every aggregate row.

The Representation column separates three kinds of value. wire is data carried in a published or decoded payload. in-memory is a client-side copy of a wire value. display projection is a rendering transform applied downstream of the wire and never fed back into it.

The Evidence column follows the same discipline as the rest of this guide. source-confirmed means the behavior is established by the pinned reference implementation. public-client policy means the public client selects a convention the pinned reference does not fix. unverified means the interpretation is neither confirmed nor contradicted by available evidence. A row covering several fields carries mixed and enumerates the evidence class of each constituent field in a note beneath its table, so an aggregate never presents a stronger class than the fields it inherits.

Three representations that appear in adjacent systems are absent here, and their absence is part of the account:

  • No orientation or attitude quaternion exists, so no ENU quaternion convention applies. Orientation is not a location field of this wire.
  • No MGRS or UTM grid reference exists.
  • No geoid model or mean-sea-level assumption exists. Altitude is ellipsoidal throughout, and no geoid separation is applied or required at runtime.

Source: src/picogrid_ecn_client/models/location.py.

Row keyValueCRS or frameAxis order and unitsAltitude referenceRepresentationEvidence
LOC-00Location.latitudeWGS-84 geographic, EPSG:4326decimal degrees, [-90, 90]wiresource-confirmed
LOC-01Location.longitudeWGS-84 geographic, EPSG:4326decimal degrees, [-180, 180]wiresource-confirmed
LOC-02Location.altitudeWGS-84 3D, EPSG:4979, where suppliedmetresheight above the WGS-84 ellipsoidwirepublic-client policy
LOC-03Location.bearingtrue north referencedegrees clockwise, [0, 360)wiresource-confirmed
LOC-04Location.accuracyhorizontal, position framemetres, drmswirepublic-client policy
LOC-05Location.recorded_attimezone-aware, normalized to UTCwiresource-confirmed
LOC-06Velocitylocal tangent plane, NED[north, east, down] metres per secondwiresource-confirmed
LOC-07AngularVelocityunresolved; the pinned source is a NED vector[roll, pitch, yaw] radians per secondwireunverified
LOC-08Location.confidencedimensionless, [0, 1]wireunverified

LOC-07 exposes three rotational values whose roll/pitch/yaw interpretation remains unverified, as recorded on the limitations page. No public conversion method consumes AngularVelocity; the protobuf decoder does rotate a canonical CRS_ECEF angular-velocity vector into NED before exposing those three values.

These rows apply wherever a Location appears, whether as a dedicated location message or embedded in an entity or entity event.

Sources: src/picogrid_ecn_client/models/location.py, src/picogrid_ecn_client/_geodesy.py.

Row keyValueCRS or frameAxis order and unitsAltitude referenceRepresentationEvidence
CNV-60ECEFPositionearth-centered earth-fixed, EPSG:4978[x, y, z] metresin-memorypublic-client policy
CNV-61GeodeticPositionWGS-84 3D, EPSG:4979decimal degrees, then metresheight above the WGS-84 ellipsoidin-memorypublic-client policy
CNV-62ECEFVelocityEPSG:4978 axes[x, y, z] metres per secondin-memorypublic-client policy
CNV-63Location.to_ecefEPSG:4979 to EPSG:4978degrees and metres, to metresheight above the WGS-84 ellipsoidin-memorypublic-client policy
CNV-64Location.to_ecef_velocityNED to EPSG:4978 axesmetres per second, both framesnot usedin-memorypublic-client policy
CNV-65GeodeticPosition rotationNED and EPSG:4978 axes, both waysmetres per second, both framesnot usedin-memorypublic-client policy

The public conversion objects in this section do not themselves reach the wire. Canonical protobuf input may mark a point as EPSG:4978 and place x/y/z metres in the latitude/longitude/altitude fields; the decoder immediately converts those values through the pinned WGS-84 path into a geodetic Location. It does not cache an ECEF object as observed state. Explicit conversions read their inputs and return new objects, so a Location is unchanged by converting it.

The ellipsoid is WGS-84 throughout: semi-major axis 6378137.0 m, inverse flattening 298.257223563. The forward conversion is the standard closed form. The inverse applies the closed-form solution of Vermeille (2011), which is exact and non-iterative, so no convergence tolerance enters the result. Both use the Python standard library only; the client takes no geodesy dependency.

CNV-64 and CNV-65 rotate a velocity between the local tangent plane and the ECEF axes. The rotation is fixed by latitude and longitude alone, so no altitude is required and none is consumed. The east axis has no ECEF Z component at any position.

Conversions hold to these bounds over the supported domain. The float64 floor at Earth radius is roughly 1.4e-9 m, so no tighter claim is meaningful.

ConversionBound
Geodetic to ECEF1e-6 m per component
ECEF to geodetic and back1e-6 m per component
Geodetic to ECEF and back1e-6 m of ECEF distance and height
Velocity, either direction1e-12 relative to the speed

Each bound names a metric. A per-component bound is the absolute error of each ECEF component in metres, against ground truth for the forward conversion and against the original vector for the ECEF round trip. ECEF distance is the Euclidean distance in ECEF metres between the original vector and the vector obtained by converting the recovered geodetic triple forward again, and height is the absolute difference of ellipsoidal height in metres.

Recovered latitude and longitude carry no metre bound. A pole has no longitude and either sign is correct on the antimeridian, so those two cases are governed by the conventions under Degenerate positions rather than by a tolerance. Away from the poles, latitude and longitude recover to within 1e-12 degrees, and on the antimeridian that bound holds on the magnitude.

The supported domain is the full range of latitude and longitude with altitude in [-11000, 100000] m. The forward conversion is defined outside that band and the inverse remains exact, but the bounds above are stated only within it.

The relative velocity bound applies to a non-zero speed. A zero north/east/down velocity converts to a zero ECEF vector exactly, and the reverse, at every orientation, because each rotated component is a sum of products with zero.

Measured against ground truth generated at 50 decimal digits, the worst observed forward error is 1.7e-9 m and the worst velocity error is 2.3e-16 relative, both at the float64 floor rather than at the stated bound. Over the same sweep, the worst geodetic round trip is 2.9e-9 m of ECEF distance and 2.3e-9 m of height.

Two inputs have no unique answer and take a documented convention rather than a tolerance.

  • On the polar axis, where x and y are both zero, longitude is undefined. It is reported as zero and latitude is reported as exactly 90 or -90. At latitude 90 or -90, the forward conversion emits exact zeros for both equatorial components, so a pole survives a round trip as longitude zero.
  • On the antimeridian, the sign of the y component decides the result: positive zero yields 180 and negative zero yields -180.

Positions within roughly 43 km of the center of the ellipsoid have no geodetic solution. ECEFPosition.to_geodetic raises ValidationError with code degenerate_ecef there rather than returning a value.

Source: src/picogrid_ecn_client/schemas/public/common.proto, LocationMessage.

Row keyValueCRS or frameAxis order and unitsAltitude referenceRepresentationEvidence
PB-10latitude, field 1EPSG:4326 or EPSG:4978latitude degrees or ECEF x metreswiresource-confirmed
PB-11longitude, field 2EPSG:4326 or EPSG:4978longitude degrees or ECEF y metreswiresource-confirmed
PB-12altitude, field 3EPSG:4979 or EPSG:4978, where suppliedellipsoidal height or ECEF z metresheight above WGS-84 when geodeticwiresource-confirmed
PB-13bearing, field 4true north referencefloat, degrees clockwisewiresource-confirmed
PB-14accuracy, field 5horizontal, position framefloat, metres, drmswirepublic-client policy
PB-15recorded_at, field 7google.protobuf.Timestamp, UTC seconds and nanosecondswiresource-confirmed
PB-16velocity, field 8NED for EPSG:4326; ECEF for EPSG:4978repeated float, three metres-per-second componentswiresource-confirmed
PB-17angular_velocity, field 9local or ECEF axes selected by CRSrepeated float, three radians-per-second componentswiresource-confirmed
PB-18confidence, field 10float, dimensionlesswireunverified
PB-19radius, field 11position framefloat, metreswiresource-confirmed
PB-20covariance, field 12position or state frameflattened 3x3, 6x6, or 9x9 repeated float matrixwiresource-confirmed
PB-21orientation, field 13selected by CRSrepeated float quaternion [w, x, y, z]wiresource-confirmed
PB-22acceleration, field 14NED for EPSG:4326; ECEF for EPSG:4978repeated float, three metres-per-second-squared valueswiresource-confirmed
PB-23crs, field 15EPSG:4326/4979 or EPSG:4978CRS_LLA or CRS_ECEF enumwiresource-confirmed
PB-24geometry_type, field 16geometry semanticspoint, line string, or polygon enumwiresource-confirmed
PB-25coordinates, field 17selected by CRS and geometry typerepeated double; ordered coordinate sequencewiresource-confirmed

Fields 1 and 2 are ordinary proto3 double scalars. Their meaning depends on crs. For CRS_ECEF, field 3 is required and the decoder interprets fields 1 through 3 as x/y/z metres before converting to geodetic coordinates. An absent crs or explicit CRS_LLA interprets fields 1 and 2 as latitude and longitude; a present CRS_UNSPECIFIED or unknown number is rejected. An omitted scalar zero cannot be distinguished from an explicit zero.

The public model retains fields 1 through 10. It accepts canonical messages carrying fields 11 through 17 without rejecting them, but does not expose those extended fields. For CRS_ECEF, the decoder consumes crs to convert the position to geodetic coordinates and rotates velocity and angular-velocity components from ECEF axes into the public model’s NED frame. Protobuf publication from the public model emits the retained geodetic subset.

PB-15 is a nested message rather than a scalar, so its presence is tracked. The producer always emits it. A dedicated location message that omits it fails decode; a location embedded in an entity event inherits the envelope timestamp instead. A decoded timestamp is always UTC-aware, and one outside the representable datetime range is rejected.

Source: src/picogrid_ecn_client/_protocol/codec.py.

Row keyValueCRS or frameAxis order and unitsAltitude referenceRepresentationEvidence
JS-20Scalar location keysas the public model rows aboveas the public model rows aboveheight above the WGS-84 ellipsoidwiremixed
JS-21velocity arraylocal tangent plane, NED[north, east, down] metres per secondwiresource-confirmed
JS-22angular_velocity arrayunresolved[roll, pitch, yaw] radians per secondwireunverified

JS-20 inherits LOC-00 through LOC-05 and LOC-08: latitude, longitude, bearing, and recorded time are source-confirmed; altitude and accuracy are public-client policy; confidence is unverified.

JS-21 and JS-22 are positional arrays. Their order is the only thing that carries axis identity, so a reordering is indistinguishable from a value change.

Source: operator-app/frontend/src/main.ts.

Row keyValueCRS or frameAxis order and unitsAltitude referenceRepresentationEvidence
DSP-30Operator map renderingWeb Mercator, EPSG:3857projected metres, then screen pixelsnot useddisplay projectionsource-confirmed

The map is constructed without a crs option, so Leaflet applies its default EPSG:3857 projection. This holds whether a configured basemap is present or the offline graticule is drawn. The projection consumes latitude and longitude only, ignores altitude, and is one-way: no projected value is written back into observed or published state. EPSG:3857 is not an ECN wire format.

Web Mercator is undefined at the poles, so the projection covers latitudes of roughly [-85.06, 85.06] and the offline graticule is drawn to that bound. A position outside that band is carried on the wire and held in memory unchanged; only its rendering is constrained.

Sources: operator-app/backend/operator_app/state.py, operator-app/frontend/src/types.ts.

Row keyValueCRS or frameAxis order and unitsAltitude referenceRepresentationEvidence
OP-40LocationView latitude, longitude, altitude, bearing, accuracy, recorded timeas the public model rows aboveas the public model rows aboveheight above the WGS-84 ellipsoidin-memorymixed

OP-40 inherits LOC-00 through LOC-05: latitude, longitude, bearing, and recorded time are source-confirmed; altitude and accuracy are public-client policy. It carries no velocity, angular velocity, or confidence value.

LocationView is populated only from decoded inbound events and carries no conversion.

Row keyValueCRS or frameAxis order and unitsAltitude referenceRepresentationEvidence
EX-50ECN_LATITUDE, ECN_LONGITUDE, ECN_ALTITUDE, ECN_BEARING, ECN_ACCURACY, ECN_CONFIDENCEas the public model rows aboveas the public model rows aboveheight above the WGS-84 ellipsoidin-memorymixed
EX-51tests/fixtures/protocol/location_update.jsonas the public model rows aboveas the public model rows aboveheight above the WGS-84 ellipsoidwiremixed

EX-50 inherits LOC-00 through LOC-04 and LOC-08: latitude, longitude, and bearing are source-confirmed; altitude and accuracy are public-client policy; confidence is unverified.

EX-51 inherits LOC-00 through LOC-03, LOC-05, LOC-07, and LOC-08: latitude, longitude, bearing, and recorded time are source-confirmed; altitude is public-client policy; angular velocity and confidence are unverified. It carries no accuracy value.

Version 0.2.0 · branch main