OGC GeoPose v1.0 Support

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 conformance classes, an explicit renormalization helper for callers running long compositions, and the Euler-angle accessors that Basic-YPR consumers expect.

JSON Input and Output

  • 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, where a quaternion member implies Basic-Quaternion while an angles member implies 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)
    

Quaternion Renormalization

  • 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)
    

Euler-Angle Accessors

  • 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.

    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)@2000-01-01,
      Pose(Point(1 1), 0.5)@2000-01-02]'));
    -- [0@2000-01-01, 0.5@2000-01-02]
    

Frame Metadata Registry

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 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 in a registry parallel to pgPointCloud's pointcloud_formats. Advanced-class frame stacks are not supported.

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 that is independent of the table layout:

  • Return the SRID for a frame, or NULL if the frame is parametric (LTP, BODY)

    geoPoseFrameSRID(int) → int

  • Return the human-readable name of a frame

    geoPoseFrameName(int) → text

  • Return true for lat/lon/h frames, false for Cartesian or projected

    geoPoseFrameIsGeographic(int) → boolean

Users can register custom frames by inserting into geopose_frames; the catalog is marked as a configuration table so pg_dump preserves user rows.

Body and World Rigid Transform

  • 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

    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. Point and multipoint body geometries are supported; lines and polygons are not. 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]
    

Temporal JSON I/O

  • Convert a temporal pose to or from its OGC GeoPose v1.0 encoding

    asGeoPose(tpose,conformance int4=0,maxdecimaldigits int4=-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 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"}]}