The pose type implements the data model behind the OGC GeoPose v1.0 standard: a position plus an orientation in a known reference frame, where the orientation is a unit quaternion in Hamilton convention or, equivalently, a yaw / pitch / roll triple under the ZYX intrinsic Tait-Bryan convention. This section groups the SQL surface that exposes that interoperability, JSON I/O for the standard's Basic and Advanced conformance classes, an explicit renormalization helper for callers running long compositions, and the Euler-angle accessors that Basic-YPR consumers expect.
Convert to or from the OGC GeoPose v1.0 JSON encoding (Basic-Quaternion, Basic-YPR or Advanced conformance class)
asGeoPose(pose,conformance integer=0,maxdecimaldigits integer=-1) → text poseFromGeoPose(text) → pose
The conformance argument selects the output class: 0 = Basic-Quaternion (default, lossless), 1 = Basic-YPR (yaw, pitch, roll in degrees, ZYX intrinsic Tait-Bryan), 2 = Advanced. The maxdecimaldigits argument is the number of significant digits to keep in the JSON numbers; -1 uses json-c's lossless default. The input function auto-detects the conformance class from the JSON keys, where a frameSpecification member implies Advanced, a quaternion member implies Basic-Quaternion, and an angles member implies Basic-YPR. The Basic classes carry the position in a position member and leave the frame implicit. The Advanced class has no position member: it names its outer frame explicitly, and the pose sits at the origin of that frame, so the two encodings of one pose read back equal. Every class mandates a geographic outer frame, so the pose must be geodetic; a planar pose is rejected at the conversion boundary, as is a projected SRID.
SELECT asGeoPose(pose 'Geodpose(Point(8 47 1500), 0.707107, 0, 0, 0.707107)', 0, 6);
/* {"position":{"lat":47,"lon":8,"h":1500},"quaternion":{"x":0,"y":0,"z":0.707107,
"w":0.707107}} */
SELECT asGeoPose(pose 'Geodpose(Point(8 47 1500), 0.707107, 0, 0, 0.707107)', 1, 6);
-- {"position":{"lat":47,"lon":8,"h":1500},"angles":{"yaw":90,"pitch":0,"roll":0}}
SELECT asGeoPose(pose 'Geodpose(Point(8 47 1500), 0.707107, 0, 0, 0.707107)', 2, 6);
/* {"frameSpecification":{"authority":"/geopose/1.0","id":"LTP-ENU","parameters":
"longitude=8&latitude=47&height=1500&crs=EPSG:4979"},
"quaternion":{"x":0,"y":0,"z":0.707107,"w":0.707107}} */
SELECT asEWKT(poseFromGeoPose( '{"position":{"lat":47,"lon":8,"h":1500},
"angles":{"yaw":90,"pitch":0,"roll":0}}'), 6);
-- SRID=4326;GeodPose(POINT Z (8 47 1500),0.707107,0,0,0.707107)
Return a pose whose orientation quaternion has been transformed to a unit norm
poseNormalize(pose) → pose
A 2D pose is returned unchanged since its orientation is a single angle. A 3D pose has its quaternion divided by its Euclidean norm, so long compositions of SLERPs (or any other path that accumulates floating-point drift in |q|) can be brought back to the |q| = 1 invariant required by the SLERP and Euler-decomposition code.
SELECT asText(poseNormalize(pose 'Pose(Point(1 1 1), 0.5, 0.5, 0.5, 0.5)')); -- Pose(POINT Z (1 1 1),0.5,0.5,0.5,0.5)
Return the inverse of a pose
poseInverse(pose) → pose
The inverse reverses the frame relationship: where a pose carries a value from the frame it names into the frame it is expressed in, the inverse carries it back. It is R_BA = R_AB^T and t_BA = −R_AB^T t_AB, so composing a pose with its inverse gives the identity. This is what expresses a world position in the frame of a vehicle. A pose over a geographic frame has no inverse: the frame it would name is not a frame of the ellipsoid.
SELECT asEWKT(round(poseInverse(pose(ST_Point(10,5), pi()/2)), 6)); -- Pose(POINT(-5 10),-1.570796) SELECT asEWKT(round(applyPose(poseInverse(pose(ST_Point(10,5), pi()/2)), pose(ST_Point(10,5), pi()/2)), 6)); -- Pose(POINT(0 0),0) SELECT asEWKT(round(poseInverse(pose 'Pose(Point(1 0 0), 0, 0, 0, 1)'), 6)); -- Pose(POINT Z (1 0 0),0,0,0,-1)
Return the yaw, pitch, or roll angle of a (temporal) pose, in radians, under the ZYX intrinsic Tait-Bryan convention required by the OGC GeoPose Basic-YPR conformance class
yaw({pose,tpose}) → {float,tfloat}
pitch({pose,tpose}) → {float,tfloat}
roll({pose,tpose}) → {float,tfloat}
For a 2D pose yaw returns the stored rotation theta, by convention the yaw of the body frame, and pitch and roll return 0. For a 3D pose the three values come from the ZYX intrinsic Tait-Bryan decomposition of the orientation quaternion. The asin pitch term is clamped to [-1, 1] to absorb the small numeric drift that long quaternion compositions can introduce. On a temporal pose the interpolation of the result follows the dimension. A 2D pose interpolates its stored angle linearly and that angle is its yaw, so a linear 2D tpose projects to a linear tfloat. A 3D pose interpolates by SLERP, whose Tait-Bryan decomposition is not linear in time, so a 3D tpose projects to a step tfloat: reading it between two instants answers the angle of the instant on the left rather than an angle no pose holds.
SELECT yaw(pose 'Pose(Point(1 1), 0.5)'); -- 0.5 SELECT pitch(pose 'Pose(Point(1 1), 0.5)'); -- 0 SELECT roll(pose 'Pose(Point(0 0 0), 0.7071067811865476, 0, 0, 0.7071067811865475)'); -- 0 SELECT yaw(pose 'Pose(Point(0 0 0), 0.7071067811865476, 0, 0, 0.7071067811865475)'); -- 1.5707963267948966
SELECT asText(yaw(tpose '[Pose(Point(0 0), 0.0)@2001-01-01, Pose(Point(1 1), 0.5)@2001-01-02]')); -- [0@2001-01-01, 0.5@2001-01-02]
Return the orientation of a pose as a yaw / pitch / roll triple, in radians
ypr(pose) → ypr
This is the OGC GeoPose Basic-YPR encoding of the orientation, the counterpart of quaternion, and it returns the three angles yaw, pitch and roll answer one at a time. Like quaternion, it is defined for both dimensions: a 2D pose yaws by its stored angle and neither pitches nor rolls. The angles are in radians, where the GeoPose JSON encoding writes them in degrees.
SELECT ypr(pose 'Pose(Point(1 1), 0.5)'); -- (0.5,0,0) SELECT ypr(pose 'Pose(Point Z(1 1 1), 1, 0, 0, 0)'); -- (0,0,0) SELECT ypr(pose 'Pose(Point Z(1 1 1), 0.5, 0.5, 0.5, 0.5)'); -- (1.5707963267948966,0,1.5707963267948966)
Lift the Euler angle accessors over a temporal pose, returning a temporal float in radians
yaw(tpose) → tfloat pitch(tpose) → tfloat roll(tpose) → tfloat
The result carries step interpolation, since an angle read from a rotation does not vary linearly between two poses.
SELECT round(yaw(tpose '[Pose(Point(0 0),0.5)@2001-01-01, Pose(Point(1 1),1.5)@2001-01-02]'), 6); -- Interp=Step;[0.5@2001-01-01, 1.5@2001-01-02] SELECT round(pitch(tpose 'Pose(Point(0 0 0),1,0,0,0)@2001-01-01'), 6); -- 0@2001-01-01
The OGC GeoPose v1.0 standard distinguishes the outer frame (the global reference, e.g., WGS-84 geographic or ECEF) from the inner frame (the body frame whose orientation is the pose's quaternion). The Basic conformance classes mandate WGS-84 geographic as the outer frame and an implicit right-handed body-axes inner frame; the Advanced class names its outer frame explicitly and places the pose at that frame's origin.
In MobilityDB the pose type encodes the outer frame implicitly via its SRID and uses the conventional right-handed body-axes inner frame. The geopose_frames table records this mapping, and states every frame identifier a document can name: a Composite Sequence Series names LTP-ENU and RotateTranslate, and a Chain names the same two frames as /Extrinsic/LTP-ENU and /Intrinsic/Translate-Rotate, all under the /geopose/1.0 authority. Advanced-class frame stacks are not supported.
SELECT frame_id, authority, code, name FROM geopose_frames ORDER BY frame_id; -- 1 | EPSG | 4326 | WGS-84 geographic (lat/lon/h) -- 2 | EPSG | 4978 | WGS-84 ECEF (Earth-Centred Earth-Fixed) -- 3 | OGC | LTP | Local Tangent Plane (East-North-Up) -- 4 | OGC | BODY | Right-handed body axes (default inner frame) -- 5 | /geopose/1.0 | LTP-ENU | GeoPose outer frame of a Composite Sequence Series -- 6 | /geopose/1.0 | RotateTranslate | GeoPose inner frame of a Composite Sequence Series -- 7 | /geopose/1.0 | /Extrinsic/LTP-ENU | GeoPose outer frame of a Chain -- 8 | /geopose/1.0 | /Intrinsic/Translate-Rotate | GeoPose inner frame of a Chain
The rows above come from MEOS, which builds them from the identifiers the encoders write, so a frame this build can emit is never absent from the registry. A user registers further frames by inserting into the table. The is_geographic column, omitted above, is true for the WGS-84 geographic frame alone.
Three SQL helpers provide a stable lookup interface that is independent of the table layout, and read the table so that a frame a user registers is found too:
Return the SRID for a frame, or NULL if the frame is parametric (LTP, BODY)
geoPoseFrameSRID(int) → int
SELECT geoPoseFrameSRID(1), geoPoseFrameSRID(2); -- 4326 | 4978 SELECT geoPoseFrameSRID(3); -- NULL
Return the human-readable name of a frame
geoPoseFrameName(int) → text
SELECT geoPoseFrameName(1); -- WGS-84 geographic (lat/lon/h) SELECT geoPoseFrameName(2), geoPoseFrameName(3); -- WGS-84 ECEF (Earth-Centred Earth-Fixed) | Local Tangent Plane (East-North-Up)
Return true for lat/lon/h frames, false for Cartesian or projected
geoPoseFrameIsGeographic(int) → boolean
SELECT geoPoseFrameIsGeographic(1), geoPoseFrameIsGeographic(2); -- true | false
Users can register custom frames by inserting into geopose_frames; the catalog is marked as a configuration table so pg_dump preserves user rows.
Applies the rigid-body transform encoded by a pose (the OGC GeoPose body to world mapping) to a body-frame geometry, producing the corresponding world-frame geometry
applyPose(geometry,pose) → geometry applyPose(geometry,tpose) → tgeompoint applyPose(pose,pose) → pose
The transform is
world = R(q) · body + p
where (p, q) are the pose's position and orientation. The static form takes a single pose and a static geometry; the temporal form lifts the per-instant rigid transform of a tpose across its instants, producing a tgeompoint world-frame trajectory of the body geometry. The static form accepts a body geometry of any type and carries its shape into the pose's frame; the temporal form returns a tgeompoint, so its body geometry must be a point. The pose and the body geometry must have the same dimensionality and must agree on their SRID, an unknown one adopting the other. Linear interpolation of the resulting trajectory is the chord on each segment, which approximates the true rigid-body trajectory (a circular arc under SLERP), the same trade-off MobilityDB already takes for spatial trajectories.
-- A body sensor offset 1 unit along the body X axis, traced through a tpose that ends 90 -- degrees yawed and translated to (10, 20). SELECT asText(applyPose(ST_Point(1,0), tpose '[Pose(Point(0 0), 0)@2026-01-01, Pose(Point(10 20), 1.5707963267948966)@2026-01-02]')); -- [POINT(1 0)@2026-01-01, POINT(10 21)@2026-01-02]
-- The shape of the body geometry is carried into the pose's frame: a hull 4 by 2 about -- the body origin, placed at (10, 20) SELECT ST_AsText(applyPose(ST_MakeEnvelope(-2,-1,2,1), pose 'Pose(Point(10 20), 0)')); -- POLYGON((8 19,8 21,12 21,12 19,8 19))
Either frame may move. Applying a pose to a tpose carries a fixed body through the whole movement of its parent, which is what a sensor mounted on a moving vehicle asks for; applying a tpose to a pose carries a moving body into a frame that stands still; and applying a tpose to a tpose composes two frames that both move, over the time they share. poseInverse lifts the same way, changing the point of view onto a moving object for the whole of its movement.
applyPose(pose,tpose) → tpose applyPose(tpose,pose) → tpose applyPose(tpose,tpose) → tpose poseInverse(tpose) → tpose
-- A sensor one unit ahead of a body that ends yawed a quarter turn at (10, 20) ends one -- unit north of it SELECT asEWKT(round(applyPose(pose 'Pose(Point(1 0), 0)', tpose '[Pose(Point(0 0), 0)@2026-01-01, Pose(Point(10 20), 1.5707963267948966)@2026-01-02]'), 6)); -- [Pose(POINT(1 0),0)@2026-01-01, Pose(POINT(10 21),1.570796)@2026-01-02]
A pose names a frame, so it is itself such a body-frame value: applying one pose to another composes the two frame relationships. Writing P_WV for the pose of a vehicle in the world and P_VS for the pose of a sensor in the vehicle, the pose of the sensor in the world is P_WS = P_WV ∘ P_VS. This is the operation a pose chain folds over its links, so a two-link chain composes to what this returns.
-- A sensor one unit ahead of a vehicle turned a quarter turn is one unit North of it, and -- carries the vehicle's orientation SELECT asEWKT(round(applyPose(pose 'Pose(Point(1 0), 0)', pose(ST_Point(0,0), pi()/2)), 6)); -- Pose(POINT(0 1),1.570796) SELECT asEWKT(round(applyPose(pose 'Pose(Point(1 0), 0)', pose(ST_Point(10,5), pi()/2)), 6)); -- Pose(POINT(10 6),1.570796) SELECT asEWKT(round(applyPose(pose 'Pose(Point(1 0 0), 1, 0, 0, 0)', pose 'Pose(Point(0 0 5), 1, 0, 0, 0)'), 6)); -- Pose(POINT Z (1 0 5),1,0,0,0)
Convert a temporal pose to or from its OGC GeoPose v1.0 encoding
asGeoPose(tpose,conformance integer=0,maxdecimaldigits integer=-1) → text tposeFromGeoPose(text) → tpose
The conformance class follows from the value. A single instant is a Basic document carrying its validTime; a sequence or sequence set is a Composite Sequence Series, of the Regular class when the instants are equally spaced by a whole number of milliseconds and of the Irregular class otherwise. The conformance argument chooses the orientation encoding of a Basic document, as it does for a pose, and has no effect on a Series, whose inner frames carry a quaternion and offer no such choice. A Series states its outer frame once, as the local tangent plane East-North-Up frame at the position of the first pose, and gives each pose as an inner frame holding its translation from that tangent point, in metres, and its rotation relative to that frame's basis. Times are GeoPose_Instant values, that is Unix time in integer milliseconds, which is what the standard requires of every time position it defines. Sub-millisecond sampling is therefore not carried; the MF-JSON encoding, which the standard behind it types as a datetime string, does carry it. The transition model reports the interpolation. Linear interpolation is the standard's interpolate model and step and discrete interpolation are its none model; since those two identifiers cannot tell step from discrete apart, the model's parameters name the interpolation with the same words the MF-JSON encoding uses.
SELECT asGeoPose(tpose '[Pose(Point(0 0 0), 0.5, 0.5, 0.5, 0.5)@2026-01-01,
Pose(Point(0 0 100), 0.5, 0.5, 0.5, 0.5)@2026-01-02,
Pose(Point(0 0 300), 0.5, 0.5, 0.5, 0.5)@2026-01-05]', 0, 6);
-- {
-- "header": {
-- "poseCount": 3,
-- "startInstant": 1767254400000,
-- "stopInstant": 1767600000000,
-- "transitionModel": {
-- "authority": "/geopose/1.0",
-- "id": "interpolate",
-- "parameters": "interpolation=Linear"
-- }
-- },
-- "outerFrame": {
-- "authority": "/geopose/1.0",
-- "id": "LTP-ENU",
-- "parameters": "longitude=0&latitude=0&height=0&crs=EPSG:4979"
-- },
-- "innerFrameAndTimeSeries": [
-- {
-- "frame": {
-- "authority": "/geopose/1.0",
-- "id": "RotateTranslate",
-- "parameters": "translation=[0, 0, 0]&rotation=[0.5, 0.5, 0.5, 0.5]"
-- },
-- "validTime": 1767254400000
-- },
-- {
-- "frame": {
-- "authority": "/geopose/1.0",
-- "id": "RotateTranslate",
-- "parameters": "translation=[0, 0, 100]&rotation=[0.5, 0.5, 0.5, 0.5]"
-- },
-- "validTime": 1767340800000
-- },
-- {
-- "frame": {
-- "authority": "/geopose/1.0",
-- "id": "RotateTranslate",
-- "parameters": "translation=[0, 0, 300]&rotation=[0.5, 0.5, 0.5, 0.5]"
-- },
-- "validTime": 1767600000000
-- }
-- ],
-- "trailer": {
-- "poseCount": 3
-- }
-- }
A Series has neither gaps nor open bounds, so a sequence set is flattened into a single closed sequence and the bounds inclusion of a sequence is not preserved. Reading also accepts the TemporalGeoPose envelope that earlier releases wrote, an array of Basic-class objects each with an added validTime, so that data already stored in that form still loads.
That envelope's shape is
{
"type": "TemporalGeoPose",
"version": "1.0",
"conformance": "Basic-Quaternion" | "Basic-YPR",
"interpolation": "None" | "Discrete" | "Step" | "Linear",
"instants": [...] // for TInstant + TSequence
"lower_inc": true|false, // for TSequence only
"upper_inc": true|false, // for TSequence only
"sequences": [{...}, ...] // for TSequenceSet only
}
SELECT asGeoPose(tpose '[Pose(Point(8 47), 0)@2026-01-01,
Pose(Point(9 48), 0.5)@2026-01-02]', 1, 4);
/* {"type":"TemporalGeoPose","version":"1.0","conformance":"Basic-YPR",
"interpolation":"Linear","lower_inc":true,"upper_inc":true,
"instants":[
{"position":{"lat":47,"lon":8,"h":0},
"angles":{"yaw":0,"pitch":0,"roll":0},
"validTime":"2026-01-01"},
{"position":{"lat":48,"lon":9,"h":0},
"angles":{"yaw":28.65,"pitch":0,"roll":0},
"validTime":"2026-01-02"}]} */
Write a temporal pose as an OGC GeoPose Stream
asGeoPoseStream(tpose,maxdecimaldigits integer=-1) → text
A stream carries the frames of an Irregular Series while stating neither how many poses there are nor when they end, since more may arrive. The standard writes it as a header, holding the transition model and the outer frame, and a streamElements array holding one element per pose. The outer frame is the local tangent plane East-North-Up frame at the position of the first pose, so the header and every element speak of one tangent point. The C library also writes the two documents a piece at a time, through tpose_as_geopose_stream_header and tpose_as_geopose_stream_element, for a producer emitting poses as they arrive. A query returns a value it already holds whole, which is what this writes.
SELECT asGeoPoseStream(tpose 'Geodpose(Point(0 0 0), 1, 0, 0, 0)@2026-01-01', 6);
/* {"header":{"transitionModel":{"authority":"/geopose/1.0","id":"none",
"parameters":"interpolation=None"},"outerFrame":{"authority":"/geopose/1.0",
"id":"LTP-ENU","parameters":"longitude=0&latitude=0&height=0&crs=EPSG:4979"}},
"streamElements":[{"streamElement":{"frame":{"authority":"/geopose/1.0",
"id":"RotateTranslate","parameters":"translation=[0, 0, 0]&rotation=[1, 0, 0, 0]"},
"validTime":1767254400000}}]} */
The standard states a composition as a Chain document and a set of frames as a Graph document, which is what a pose chain writes.
Write or read an OGC GeoPose Composite Chain document
asGeoPose(tposechain,maxdecimaldigits integer=-1) → text tposechainFromGeoPose(text) → tposechain
A Chain document is an outer frame and a sequence of transformations reaching a final innermost frame, which is what a pose chain holds. The document names the LTP-ENU frame tangent at the outer link's position and one transformation per link: the first takes that tangent frame to the outer link's own frame, and each later one is the link as it is stored, read in the axes of its parent. The document carries one valid time, so it is written from a single instant; atTime obtains one from a longer value. Its frame chain holds at least two frames, so a chain of one link has no Chain document. The Chain class names its two frames /Extrinsic/LTP-ENU and /Intrinsic/Translate-Rotate, where the Advanced, Series and Stream classes name the same two frames LTP-ENU and RotateTranslate. A document is written with the pair its own class uses, and either pair is accepted on reading.
SELECT asGeoPose(tposechain 'SRID=4326;
PoseChain( GeodPose(Point Z(-122.3 47.7 11), 1, 0, 0, 0),
Pose(Point Z(2 0 0), 1, 0, 0, 0))@2021-04-28 05:36:10.083+00', 6);
/* {"validTime":1619588170083,"outerFrame":{"authority":"/geopose/1.0",
"id":"/Extrinsic/LTP-ENU","parameters":"longitude=-122.3&latitude=47.7&
height=11&crs=EPSG:4979"},"frameChain":[{"authority":"/geopose/1.0",
"id":"/Intrinsic/Translate-Rotate","parameters":"translation=[0, 0, 0]&
rotation=[1, 0, -1.38778e-17, 0]"},{"authority":"/geopose/1.0",
"id":"/Intrinsic/Translate-Rotate","parameters":"translation=[2, 0, 0]&
rotation=[1, 0, 0, 0]"}]} */
Write an OGC GeoPose Composite Graph document
asGeoPose(tposechain[],maxdecimaldigits integer=-1) → text
A Graph document is a set of frames and the transformations between them, which several pose chains sharing their outermost frame hold. The frame list names the LTP-ENU frame tangent at that outermost link followed by the links of every chain, and the transform list names the parent and the child of each edge by their position in that list. The edges carry no transformation of their own; it lives in the frames they name. The document carries one valid time, so it is written from chains read at one and the same instant; atTime obtains them from longer values. Its frame list holds at least two frames, which one link of one chain already gives beside the tangent frame.
SELECT asGeoPose(ARRAY[tposechain 'SRID=4326;
PoseChain( GeodPose(Point Z(-122.3 47.7 11), 1, 0, 0, 0),
Pose(Point Z(2 0 0), 1, 0, 0, 0))@2021-04-28 05:36:10.083+00',
tposechain 'SRID=4326;
PoseChain(GeodPose(Point Z(-122.3 47.7 11), 1, 0, 0, 0),
Pose(Point Z(0 3 0), 1, 0, 0, 0))@2021-04-28 05:36:10.083+00'], 6);
/* {"validTime":1619588170083,"frameList":[{"authority":"/geopose/1.0",
"id":"/Extrinsic/LTP-ENU","parameters":"longitude=-122.3&latitude=47.7&
height=11&crs=EPSG:4979"},{"authority":"/geopose/1.0",
"id":"/Intrinsic/Translate-Rotate","parameters":"translation=[0, 0, 0]&
rotation=[1, 0, -1.38778e-17, 0]"},{"authority":"/geopose/1.0",
"id":"/Intrinsic/Translate-Rotate","parameters":"translation=[2, 0, 0]&
rotation=[1, 0, 0, 0]"},{"authority":"/geopose/1.0",
"id":"/Intrinsic/Translate-Rotate","parameters":"translation=[0, 0, 0]&
rotation=[1, 0, -1.38778e-17, 0]"},{"authority":"/geopose/1.0",
"id":"/Intrinsic/Translate-Rotate","parameters":"translation=[0, 3, 0]&
rotation=[1, 0, 0, 0]"}],"transformList":[{"link":[0,1]},{"link":[1,2]},
{"link":[0,3]},{"link":[3,4]}]} */