Table of Contents
The pose type is used to represent the location and orientation of geometric objects within coordinate systems anchored to the earth's surface or within other astronomical coordinate systems. The location is represented by a 2D or 3D point. For 2D poses, the orientation is defined by a rotation angle in (-π, π] expressed in radians. For 3D poses, the orientation is defined by four float values W, X, Y, Z, representing a unit quaternion
where
.
The pose constructor accepts quaternions whose norm is within 1e-3 of unit (a tolerance wide enough to absorb the integrator drift typical of sensor-fusion clients such as IMUs and AR/VR runtimes), then renormalizes to exactly unit norm before storing. Way-off norms (e.g., the obvious-bug case (1, 1, 1, 1) where |q| = 2), zero quaternions, and quaternions with NaN/Inf components are rejected up front. The on-disk representation is therefore independent of the caller's floating-point hygiene, and downstream comparison, hashing, SLERP, and Euler-decomposition code can rely on the unit-norm invariant.
The GeoPose Standards Working Group (SWG), working under the auspices of the Open Geospatial Consortium, has defined a standard for exchanging pose information across different users, devices, and platforms. More information about the standard can be found in the GitHub repository of the GeoPose SWG.
The pose type serves as base type for defining the temporal pose type tpose. The tpose type has similar functionality as the temporal point type tgeompoint. Thus, most functions and operators described before for the tgeompoint type are also applicable for the tpose type. In addition, there are specific functions defined for the tpose type.
The tpose type is used for defining the type trgeometry (that is, temporal rigid geometry) defined in the next chapter. The implementation of the these types in MobilityDB has been studied in the following PhD thesis.
A 2D pose is a couple of the form (point2D,radius) where point2D is a 2D geometric point and radius is a float value representing a rotation angle in (-π, π] expressed in radians. A 3D pose is a tuple of the form (point3D,W,Y,X,Z) where point3D is a 3D geometric point, and W, and X, Y, and Z are four floats representing a unit quaternion. Examples of input of pose values are as follows:
SELECT pose 'Pose(Point(1 1), 0.5)'; SELECT pose 'Pose(Point Z(1 1 1), 0.5, 0.5, 0.5, 0.5)';
An SRID can be specified for a pose either at the begining of the pose literal or before the point literal as shown below.
SELECT pose 'SRID=3812;Pose(Point(1 1), 0.5)'; SELECT pose 'Pose(SRID=5676;Point Z(1 1 1), 0.5, 0.5, 0.5, 0.5)';
As for other spatial types, a pose can be either planar (geometric) or geodetic (geographic). A geodetic pose is denoted by the GeodPose keyword, in the same way that a geodetic stbox is denoted by GEODSTBOX.
SELECT pose 'GeodPose(Point(1 1), 0.5)'; SELECT pose 'SRID=4326;GeodPose(Point Z(1 1 1), 1, 0, 0, 0)';
Values of the pose type must satisfy several constraints so that they are well defined. Examples of incorrect pose type values are as follows.
-- Empty point select pose 'Pose(Point empty, 0.5)'; -- Incorrect point value SELECT pose 'Pose(Linestring(1 1,2 2), 1.0)'; -- Incorrect radius value SELECT pose 'Pose(Point(1 1), -10.0)'; -- Incorrect 3D point SELECT pose 'Pose(Point Z(1 1), 1.0)'; -- Incomplete 3D orientation SELECT pose 'Pose(Point Z(1 1 1), 1.0)';
We give next the functions and operators for the pose type.
Return the Well-Known Text (WKT) or the Extended Well-Known Text (EWKT) representation
asText({pose,pose[]}) → {text,text[]}
asEWKT({pose,pose[]}) → {text,text[]}
SELECT asText(pose 'SRID=4326;Pose(Point(0 0),1)');
-- Pose(POINT(0 0),1)
SELECT asText(ARRAY[pose 'Pose(Point(0 0),1)', 'Pose(Point(1 1),2)']);
-- {"Pose(POINT(0 0),1)","Pose(POINT(1 1),2)"}
SELECT asEWKT(pose 'SRID=4326;Pose(Point(0 0),1)');
-- SRID=4326;Pose(Point(0 0),1)
SELECT asEWKT(ARRAY[pose 'Pose(SRID=5676;Point(0 0),1)', 'Pose(SRID=5676;Point(1 1),2)']);
-- {"Pose(SRID=5676;POINT(0 0),1)","Pose(SRID=5676;POINT(1 1),2))"}
Return the Well-Known Binary (WKB), the Extended Well-Known Binary (EWKB), or the Hexadecimal Extended Well-Known Binary (HexEWKB) representation
asBinary(pose,endian text='') → bytea
asEWKB(pose,endian text='') → bytea
asHexEWKB(pose,endian text='') → text
The result is encoded using either the little-endian (NDR) or the big-endian (XDR) encoding. If no encoding is specified, then the encoding of the machine is used.
SELECT asBinary(pose 'Pose(Point(1 2),1)'); -- \x0101000000000000f03f0000000000000040000000000000f03f SELECT asEWKB(pose 'SRID=7844;Pose(Point(1 2),1)'); -- \x0141a41e0000000000000000f03f0000000000000040000000000000f03f SELECT asHexEWKB(pose 'SRID=3812;Pose(Point(1 2),1)'); -- 0141E40E0000000000000000F03F0000000000000040000000000000F03F
Input from the Well-Known Text (WKT) or from the Extended Well-Known Text (EWKT) representation
poseFromText(text) → pose
poseFromEWKT(text) → pose
SELECT asEWKT(poseFromText(text 'Pose(Point(1 2),1)')); -- Pose(POINT(1 2),1) SELECT asEWKT(poseFromEWKT(text 'SRID=3812;Pose(Point(1 2),1)')); -- SRID=3812;Pose(Point(1 2),1)
Input from the Well-Known Binary (WKB), from the Extended Well-Known Binary (EWKB), or from the Hexadecimal Extended Well-Known Binary (HexEWKB) representation
poseFromBinary(bytea) → pose
poseFromEWKB(bytea) → pose
poseFromHexEWKB(text) → pose
SELECT asEWKT(poseFromBinary( '\x0101000000000000f03f0000000000000040000000000000f03f')); -- Pose(POINT(1 2),1) SELECT asEWKT(poseFromEWKB( '\x0141a41e0000000000000000f03f0000000000000040000000000000f03f')); -- SRID=7844;Pose(Point(1 2),1) SELECT asEWKT(poseFromHexEWKB( '0141E40E0000000000000000F03F0000000000000040000000000000F03F')); -- SRID=3812;Pose(POINT(1 2),1)
Constructor for poses
pose(geompoint2D,float) → pose
pose(geompoint3D,float,float,float,float) → pose
SELECT asText(pose(ST_Point(1,1), radians(45)), 6); -- Pose(POINT(1 1),0.785398) SELECT asEWKT(pose(ST_Point(1,1,3812), radians(45)), 6); -- SRID=3812;Pose(POINT(1 1),0.785398) SELECT asText(pose(ST_PointZ(1,1,1), 1, 0, 0, 0)); -- Pose(POINT Z (1 1 1),1,0,0,0)
Values of the pose type can be converted to the geometry point type using an explicit CAST or using the :: notation as shown below.
Convert a pose and, optionally, a timestamp or a period, into a spatiotemporal box
pose::stbox
stbox(pose) → stbox
stbox(pose,{timestamptz,tstzspan}) → stbox
SELECT stbox(pose 'SRID=5676;Pose(Point(1 1),0.3)'); -- SRID=5676;STBOX X((1,1),(1,1)) SELECT stbox(pose 'Pose(Point(1 1),0.3)', timestamptz '2001-01-01'); -- STBOX XT(((1,1),(1.3,1.3)),[2001-01-01, 2001-01-01]) SELECT stbox(pose 'Pose(Point(1 1),0.3)', tstzspan '[2001-01-01,2001-01-02]'); -- STBOX XT(((1,1),(1.3,1.3)),[2001-01-01, 2001-01-02])
Convert a pose into geometry point
pose::geompoint
SELECT ST_AsText(pose(ST_Point(1, 1), 1)::geometry); -- Point(1 1) SELECT ST_AsEWKT(pose(ST_PointZ(1, 1, 1, 5676), 1, 0, 0, 0)::geometry); -- SRID=5676;POINT(1 1 1)
Return the point
point(pose) → geompoint
SELECT ST_AsText(point(pose 'Pose(Point(1 1), 0.3)')); -- Point(1 1)
Return the rotation
rotation(pose2D) → float
SELECT rotation(pose 'Pose(Point(1 1), 0.3)'); -- 0.3
Return the orientation
orientation(pose3D) → (X,W,Z,T)
SELECT orientation(pose 'Pose(Point Z(1 1 1), 0, 0, 0, 1)'); -- (0, 0, 0, 1)
Return or set the spatial reference identifier
srid(pose) → integer
setSRID(pose) → pose
SELECT SRID(pose 'Pose(SRID=5676;Point(1 1), 0.3)'); -- 5676 SELECT asEWKT(setSRID(pose 'Pose(Point(0 0),1)', 4326)); -- SRID=4326;Pose(POINT(0 0),1)
Transform to a spatial reference identifier
transform(pose,integer) → pose
transformPipeline(pose,pipeline text,to_srid integer,is_forward bool=true) → pose
The transform function specifies the transformation with a target SRID. An error is raised when the input pose has an unknown SRID (represented by 0).
The transformPipeline function specifies the transformation with a defined coordinate transformation pipeline represented with the following string format:
urn:ogc:def:coordinateOperation:AUTHORITY::CODE
The SRID of the input pose is ignored, and the SRID of the output pose will be set to zero unless a value is provided via the optional to_srid parameter. As stated by the last parameter, the pipeline is executed by default in a forward direction; by setting the parameter to false, the pipeline is executed in the inverse direction.
SELECT asEWKT(transform(pose 'SRID=4326;Pose(Point(4.35 50.85),1)', 3812), 6); -- SRID=4326;Pose(POINT(648679.018035 671067.055638),1)
WITH test(pose, pipeline) AS (
SELECT pose 'Pose(SRID=4326;Point(4.3525 50.846667),1)',
text 'urn:ogc:def:coordinateOperation:EPSG::16031' )
SELECT asEWKT(transformPipeline(transformPipeline(pose, pipeline, 4326), pipeline,
4326, false), 6)
FROM test;
-- SRID=4326;Pose(POINT(4.3525 50.846667),1)
The comparison operators (=, <, and so on) are available for poses. Excepted the equality and inequality, the other comparison operators are not useful in the real world but allow B-tree indexes to be constructed on poses.
Traditional comparisons
pose {=, <>, <, >, <=, >=} pose
SELECT pose 'Pose(Point(3 3), 0.5)' = pose 'Pose(Point(3 3), 0.5)'; -- true SELECT pose 'Pose(Point(3 3), 0.5)' <> pose 'Pose(Point(3 3), 0.6)'; -- true SELECT pose 'Pose(Point(3 3), 0.5)' < pose 'Pose(Point(3 3), 0.6)'; -- true SELECT pose 'Pose(Point(3 3), 0.6)' > pose 'Pose(Point(2 2), 0.6)'; -- true SELECT pose 'Pose(Point Z(1 1 1), 0.5, 0.5, 0.5, 0.5)' <= pose 'Pose(Point Z(2 2 2), 0.5, 0.5, 0.5, 0.5)'; -- true SELECT pose 'Pose(Point(1 1), 0.6)' >= pose 'Pose(Point(1 1), 0.5)'; -- true
Are the poses approximately equal with respect to an epsilon value?
pose ~= pose → boolean
SELECT pose 'Pose(SRID=5676;Point(1 1), 0.3)' ~= pose 'Pose(SRID=5676;Point(1 1.0000001), 0.30000001)'; -- true
MobilityDB's pose type implements the data model behind the OGC GeoPose v1.0 standard (21-056r10): 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 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 or Basic-YPR conformance class)
asGeoPose(pose,conformance int4=0,maxdecimaldigits int4=-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). 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 (quaternion member → Basic-Quaternion, angles member → Basic-YPR). The Basic conformance classes mandate a geographic outer frame; the implementation accepts SRID 4326 (or 0, treated as geographic) and rejects projected SRIDs at the conversion boundary.
SELECT asGeoPose(pose 'Pose(Point(8 47 1500), 0.7071067811865476, 0, 0, 0.7071067811865475)', 0, 6);
-- {"position":{"lat":47,"lon":8,"h":1500},"quaternion":{"x":0,"y":0,"z":0.707107,"w":0.707107}}
SELECT asGeoPose(pose 'Pose(Point(8 47 1500), 0.7071067811865476, 0, 0, 0.7071067811865475)', 1, 6);
-- {"position":{"lat":47,"lon":8,"h":1500},"angles":{"yaw":90,"pitch":0,"roll":0}}
SELECT asEWKT(poseFromGeoPose(
'{"position":{"lat":47,"lon":8,"h":1500},"angles":{"yaw":90,"pitch":0,"roll":0}}'));
-- Pose(POINT Z (8 47 1500),0.7071067811865476,0,0,0.7071067811865475)
Return a pose whose orientation quaternion has been brought back to unit norm. A 2D pose is returned unchanged since its orientation is a single angle that does not drift. 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.
poseNormalize(pose) → pose
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 yaw, pitch, or roll angle of a pose, in radians, under the ZYX intrinsic Tait-Bryan convention required by the OGC GeoPose Basic-YPR conformance class
yaw(pose) → float
pitch(pose) → float
roll(pose) → float
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.
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
Lift the Euler-angle accessors through a temporal pose, returning a temporal float (radians) per instant under the ZYX intrinsic Tait-Bryan convention
yaw(tpose) → tfloat
pitch(tpose) → tfloat
roll(tpose) → tfloat
SELECT asText(yaw(tpose '[Pose(Point(0 0), 0.0)@2000-01-01, Pose(Point(1 1), 0.5)@2000-01-02]')); -- [0@2000-01-01, 0.5@2000-01-02]
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 supports stacks of named frames.
In MobilityDB v1 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 documents this mapping and seeds a registry parallel to pgPointCloud's pointcloud_formats, so that future Advanced-class support only needs to extend the catalog instead of redesigning it.
SELECT frame_id, authority, code, name, is_geographic FROM geopose_frames ORDER BY frame_id; -- 1 | EPSG | 4326 | WGS-84 geographic (lat/lon/h) | t -- 2 | EPSG | 4978 | WGS-84 ECEF (Earth-Centred Earth-Fixed) | f -- 3 | OGC | LTP | Local Tangent Plane (East-North-Up) | f -- 4 | OGC | BODY | Right-handed body axes (default inner frame) | f
Three SQL helpers provide a stable lookup interface for code that doesn't want to hard-code the table layout:
geopose_frame_srid(int) → int — the PostGIS SRID for a frame, or NULL if the frame is parametric (LTP, BODY).
geopose_frame_name(int) → text — the human-readable name of the frame.
geopose_frame_is_geographic(int) → boolean — true for lat/lon/h frames, false for Cartesian or projected.
Users can register custom frames by inserting into geopose_frames; the catalog is marked as a configuration table so pg_dump preserves user rows.
The applyPose function applies the rigid-body transform encoded by a pose (the OGC GeoPose body→world mapping) to a body-frame geometry, producing the corresponding world-frame geometry. 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. v1 supports point and multipoint body geometries; lines and polygons are deferred. 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.
applyPose(geometry, pose) → geometry
applyPose(geometry, tpose) → tgeompoint
-- 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 transform(pose, srid) function carries pose values across SRIDs. Workstream #6 of the temporal-GeoPose plan extends the pre-existing position-only behaviour with the orientation correction required when the source and target frames have different bases at the pose's point. v1 implements the canonical OGC GeoPose case — WGS-84 geographic (EPSG:4326) ↔ WGS-84 ECEF (EPSG:4978) — using the standard East-North-Up basis at the geographic point as the rotation pivot. For SRID pairs whose orientation correction is not yet defined, transform emits a NOTICE and passes the orientation through unchanged.
Round-tripping through ECEF and back lands exactly at the input pose:
SELECT asEWKT(round(
transform(transform(pose 'SRID=4326;Pose(Point(8 47 0), 1, 0, 0, 0)', 4978),
4326), 6));
-- SRID=4326;Pose(POINT Z (8 47 0),1,0,0,0)
-- At the equator-meridian (lat=lon=0), the body identity quaternion in
-- the ECEF basis becomes the canonical ENU->ECEF rotation:
SELECT asEWKT(round(
transform(pose 'SRID=4326;Pose(Point(0 0 0), 1, 0, 0, 0)', 4978), 6));
-- SRID=4978;Pose(POINT Z (6378137 0 0),0.5,-0.5,-0.5,-0.5)
The implementation also fixes a pre-existing position-only bug where transform did not update the result's SRID field, so the returned pose now carries the target SRID as expected.
Convert a temporal pose to or from a TemporalGeoPose JSON envelope. Each per-instant payload is a strictly OGC-valid Basic-class GeoPose document plus a validTime member; the envelope records the conformance class, the interpolation, and (for sequences and sequence sets) the period bounds inclusion. A non-MEOS GeoPose consumer can iterate instants[] (or each sequences[].instants[]) and consume each element as a static GeoPose document.
asGeoPose(tpose,conformance int4=0,maxdecimaldigits int4=-1) → text
tposeFromGeoPose(text) → tpose
The envelope 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 00:00:00+01"},
-- {"position":{"lat":48,"lon":9,"h":0},
-- "angles":{"yaw":28.65,"pitch":0,"roll":0},
-- "validTime":"2026-01-02 00:00:00+01"}]}
Return the speed of a temporal pose as a temporal float — the magnitude of the position-component velocity (distance travelled per unit time). The orientation is not part of this scalar; for angular velocity see angularSpeed.
speed(tpose) → tfloat
SELECT asText(speed(tpose '[Pose(Point(0 0), 0)@2000-01-01, Pose(Point(1 1), 0.5)@2000-01-02]')); -- Interp=Step;[1.6368e-5@2000-01-01, 1.6368e-5@2000-01-02] -- (sqrt(2) units / 86400 seconds — units depend on the SRID's CRS)
Return the angular speed of a temporal pose as a step-interpolated temporal float (radians per unit time). For 2D poses this is the per-segment shortest-arc theta delta divided by the segment duration; for 3D poses it is the SLERP arc angle 2 · acos(|q1 · q2|) divided by the segment duration. SLERP is by construction constant-angular-velocity along a segment, so the result is piecewise-constant.
angularSpeed(tpose) → tfloat
-- 90-degree yaw rotation over one day -> pi/2 rad / 86400 s
SELECT asText(angularSpeed(tpose
'[Pose(Point(0 0 0), 1, 0, 0, 0)@2000-01-01,
Pose(Point(0 0 0), 0.7071067811865476, 0, 0, 0.7071067811865475)@2000-01-02]'));
-- Interp=Step;[1.8181e-5@2000-01-01, 1.8181e-5@2000-01-02]