Skip to content

Implement P2/P3 annotation types (POLY, ELLIPSE, TIMETEXT, DYNAMIC, SENSOR, TMA) #56

@IanMayo

Description

@IanMayo

Summary

Implement the remaining annotation types deferred from Feature 007 (REP File Special Comments). The P1 types (NARRATIVE, CIRCLE, RECT, LINE, VECTOR, TEXT) are complete with 189 passing tests. This issue covers P2 and P3 annotation types.

Background

Feature 007 established the annotation parsing infrastructure:

  • Coordinate parsing (DMS format)
  • Timestamp parsing (YYMMDD HHMMSS with Y2K handling)
  • Symbol parsing (all 5 formats: @x, @ba10, @x[LAYER=y], aX, 0X)
  • Color mapping (A-Q → CSS hex)
  • Fail-fast validation

This infrastructure is reused for all remaining types.

Priority Groups

P2 - Common Annotation Types

  • POLY - Multi-vertex closed polygon
  • POLYLINE - Multi-vertex open line
  • ELLIPSE - Timed ellipse with orientation
  • ELLIPSE2 - Ellipse with time range
  • TIMETEXT - Text with single timestamp
  • PERIODTEXT - Text with time range
  • WHEEL - Annular region (donut shape)

P3 - Specialized Types

  • DYNAMIC_RECT - Time-varying rectangle
  • DYNAMIC_CIRCLE - Time-varying circle
  • DYNAMIC_POLY - Time-varying polygon
  • SENSOR - Sensor contact (bearing/range)
  • SENSOR2 - Extended sensor format
  • TMA_POS - TMA position fix with ellipse
  • TMA_RB - TMA range/bearing
  • TRACKSPLIT - Track split marker

P2 Annotation Specifications

POLY (Closed Polygon)

REP Format:
```
;POLY: @ga30 21.9 0 0 N 21.5 0 0 W 22 0 0 N 21.8 0 0 W 22.1 0 0 N 21.5 0 0 W test\npoly
;POLY: SYMBOL VERTEX1_LAT VERTEX1_LON [VERTEX2...] LABEL
```

Fields:

Field Format Required Description
SYMBOL @x or extended Yes Symbol code with styling
VERTICES DMS pairs Yes 3+ lat/lon pairs
LABEL String No May contain \n escapes

GeoJSON Output:
```json
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[lon1, lat1], [lon2, lat2], ..., [lon1, lat1]]]
},
"properties": {
"kind": "POLYGON",
"label": "test poly",
"style": { "color": "#800080", "fill": true, ... }
}
}
```

Notes:

  • Minimum 3 vertices required
  • Coordinates automatically closed (first point repeated at end)
  • Label may contain literal `\n` which should be preserved

POLYLINE (Open Line)

REP Format:
```
;POLYLINE: @C 21.1 0 0 N 21.5 0 0 W 21.2 0 0 N 21.8 0 0 W 21.3 0 0 N 21.5 0 0 W test\npolyline
;POLYLINE: SYMBOL VERTEX1_LAT VERTEX1_LON [VERTEX2...] LABEL
```

GeoJSON Output:
```json
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[lon1, lat1], [lon2, lat2], ...]
},
"properties": {
"kind": "POLYLINE",
"label": "test polyline",
"style": { "color": "#FF0000", "weight": 1, ... }
}
}
```

Notes:

  • Minimum 2 vertices required
  • NOT closed (unlike POLY)

ELLIPSE (Timed Ellipse)

REP Format:
```
;ELLIPSE: @f[LAYER=TUAs] 951212 055200 21.4 0 0 N 21.1 0 0 W 65.0 5000 3000 test ellipse
;ELLIPSE: SYMBOL YYMMDD HHMMSS LAT_DMS LON_DMS ORIENTATION_DEG MAJOR_M MINOR_M LABEL
```

Fields:

Field Format Required Description
SYMBOL @x[...] Yes May include LAYER attribute
DATE YYMMDD Yes Date (50-99→1900s, 00-49→2000s)
TIME HHMMSS Yes Time
LAT DMS Yes Center latitude
LON DMS Yes Center longitude
ORIENTATION Decimal Yes Degrees from north (0-360)
MAJOR Decimal Yes Semi-major axis in meters
MINOR Decimal Yes Semi-minor axis in meters
LABEL String No Text label

GeoJSON Output:
```json
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[...32+ points approximating ellipse...]]]
},
"properties": {
"kind": "ELLIPSE",
"time": "1995-12-12T05:52:00+00:00",
"center": [-21.1, 21.4],
"orientation": 65.0,
"semi_major_axis": 5000,
"semi_minor_axis": 3000,
"layer": "TUAs",
"label": "test ellipse",
"style": { ... }
}
}
```

Geometry Generation:

  • Generate 32+ points around ellipse perimeter
  • Apply rotation transformation for orientation
  • Formula: x = acos(θ), y = bsin(θ), then rotate by orientation

ELLIPSE2 (Ellipse with Time Range)

REP Format:
```
;ELLIPSE2: @g[LAYER=TUAs] 951212 060400 951212 061200 21.9 0 0 N 21.5 0 0 W 85.0 6000 2000 test ellipse 2
;ELLIPSE2: SYMBOL START_DATE START_TIME END_DATE END_TIME LAT LON ORIENTATION MAJOR MINOR LABEL
```

Additional Fields vs ELLIPSE:

Field Format Description
START_DATE YYMMDD Period start date
START_TIME HHMMSS Period start time
END_DATE YYMMDD Period end date
END_TIME HHMMSS Period end time

GeoJSON Output:
Same as ELLIPSE but with:
```json
"properties": {
"kind": "ELLIPSE",
"start_time": "1995-12-12T06:04:00+00:00",
"end_time": "1995-12-12T06:12:00+00:00",
...
}
```


TIMETEXT (Text with Timestamp)

REP Format:
```
;TIMETEXT: @C 951212 050200 21.7 0 0 N 21.7 0 0 W test timetext
;TIMETEXT: SYMBOL YYMMDD HHMMSS LAT_DMS LON_DMS TEXT
```

GeoJSON Output:
```json
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-21.7, 21.7]
},
"properties": {
"kind": "TIMETEXT",
"time": "1995-12-12T05:02:00+00:00",
"text": "test timetext",
"style": { ... }
}
}
```


PERIODTEXT (Text with Time Range)

REP Format:
```
;PERIODTEXT: @C 951212 050200 951212 060200 21.7 0 0 N 21.2 0 0 W test period 1
;PERIODTEXT: SYMBOL START_DATE START_TIME END_DATE END_TIME LAT LON TEXT
```

GeoJSON Output:
```json
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-21.2, 21.7]
},
"properties": {
"kind": "PERIODTEXT",
"start_time": "1995-12-12T05:02:00+00:00",
"end_time": "1995-12-12T06:02:00+00:00",
"text": "test period 1",
"style": { ... }
}
}
```


WHEEL (Annular Region)

REP Format:
```
;WHEEL: @C 951212 050200 21.3 0 0 N 21.5 0 0 W 200 1500 test wheel
;WHEEL: SYMBOL YYMMDD HHMMSS LAT_DMS LON_DMS INNER_RADIUS OUTER_RADIUS LABEL
```

Fields:

Field Format Description
INNER_RADIUS Decimal Inner radius in meters
OUTER_RADIUS Decimal Outer radius in meters

GeoJSON Output:
```json
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [
[[...outer ring points...]],
[[...inner ring points (hole)...]]
]
},
"properties": {
"kind": "WHEEL",
"time": "1995-12-12T05:02:00+00:00",
"center": [-21.5, 21.3],
"inner_radius": 200,
"outer_radius": 1500,
"label": "test wheel",
"style": { ... }
}
}
```

Geometry Generation:

  • Outer ring: 32+ points at outer_radius
  • Inner ring (hole): 32+ points at inner_radius, wound OPPOSITE direction

P3 Annotation Specifications

DYNAMIC_RECT (Time-Varying Rectangle)

REP Format:
```
;DYNAMIC_RECT: @A "Dynamic A" 951212 051000.000 22 00 0 N 21 00 0 W 21 50 0 N 20 50 0 W dynamic A rect 1
;DYNAMIC_RECT: SYMBOL "NAME" YYMMDD HHMMSS.SSS CORNER1_LAT CORNER1_LON CORNER2_LAT CORNER2_LON LABEL
```

Key Features:

  • Quoted name identifies the dynamic shape across multiple entries
  • Multiple entries with same name = same shape at different times
  • Timestamp supports milliseconds (HHMMSS.SSS)

Grouping Logic:

  1. Parse all DYNAMIC_RECT entries
  2. Group by quoted name
  3. Each group becomes one feature with multiple time positions

GeoJSON Output:
```json
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[...]]]
},
"properties": {
"kind": "DYNAMIC_RECT",
"name": "Dynamic A",
"positions": [
{
"time": "1995-12-12T05:10:00.000+00:00",
"geometry": { "type": "Polygon", "coordinates": [...] },
"label": "dynamic A rect 1"
},
{
"time": "1995-12-12T05:15:00.000+00:00",
"geometry": { "type": "Polygon", "coordinates": [...] },
"label": "dynamic A rect 2"
}
],
"style": { ... }
}
}
```

Alternative: Emit one feature per time position with shared `name` property for client-side grouping.


DYNAMIC_CIRCLE (Time-Varying Circle)

REP Format:
```
;DYNAMIC_CIRCLE: @A "Dynamic A" 951212 052100.000 21 00 0 N 20 53 0 W 2000 dynamic A circ 12
;DYNAMIC_CIRCLE: SYMBOL "NAME" YYMMDD HHMMSS.SSS LAT_DMS LON_DMS RADIUS_M LABEL
```

Same grouping logic as DYNAMIC_RECT.


DYNAMIC_POLY (Time-Varying Polygon)

REP Format:
```
;DYNAMIC_POLY: @A "Dynamic A" 951212 052600.000 20 35 0 N 21 02 0 W 20 35 0 N 20 55 0 W ... label
;DYNAMIC_POLY: SYMBOL "NAME" YYMMDD HHMMSS.SSS VERTICES... LABEL
```

Same grouping logic as DYNAMIC_RECT.


SENSOR (Sensor Contact)

REP Format:
```
;SENSOR: 951212 051100 "NEL STYLE" @A 22 2 27.78 N 21 1 13.78 W -13.9 12000 Plain Cookie SUBJECT held on Plain Cookie
;SENSOR: YYMMDD HHMMSS "TRACK" SYMBOL LAT LON BEARING RANGE SENSOR_TYPE LABEL
```

Fields:

Field Format Required Description
DATE YYMMDD Yes Detection date
TIME HHMMSS Yes Detection time
TRACK Quoted Yes Ownship/sensor platform name
SYMBOL @x Yes Symbol code
LAT DMS Yes Contact position latitude
LON DMS Yes Contact position longitude
BEARING Decimal Yes Bearing to contact (can be negative?)
RANGE Decimal Yes Range in meters
SENSOR_TYPE String Yes Type identifier (e.g., "Plain Cookie")
LABEL String No Description text

GeoJSON Output:
```json
{
"type": "Feature",
"geometry": {
"type": "Point",
"coordinates": [-21.02, 22.04]
},
"properties": {
"kind": "SENSOR",
"time": "1995-12-12T05:11:00+00:00",
"track_id": "NEL STYLE",
"bearing": -13.9,
"range": 12000,
"sensor_type": "Plain Cookie",
"label": "SUBJECT held on Plain Cookie",
"style": { ... }
}
}
```


SENSOR2 (Extended Sensor Format)

REP Format:
```
;SENSOR2: 951212 051400.000 NEL_STYLE2 @b NULL 59.3 300.8 49.96 NULL SENSOR Contact_bearings 0414
;SENSOR2: YYMMDD HHMMSS.SSS TRACK SYMBOL FREQ? BEARING? RANGE? SPEED? DEPTH? TYPE LABEL
```

Notes:

  • Fields may contain NULL for missing values
  • Track name is NOT quoted (unlike SENSOR)
  • More fields than SENSOR, meanings need verification from legacy Debrief

Parsing Strategy:

  • Treat NULL as Python None
  • Parse what we can, preserve raw values for unknown fields

TMA_POS (TMA Position Fix)

REP Format:
```
;TMA_POS: 951212 051200.000 "NEL STYLE" @e 22 12 10.14 N 21 34 27.62 W TARGET 130 800 300 012 4 100 800x300
;TMA_POS: YYMMDD HHMMSS.SSS "TRACK" SYMBOL LAT LON TARGET_NAME ORIENT MAJOR MINOR COURSE SPEED DEPTH LABEL
```

Fields:

Field Format Description
TRACK Quoted Ownship track name
TARGET_NAME String Target track identifier
ORIENT Decimal Ellipse orientation (degrees)
MAJOR Decimal Semi-major axis (meters)
MINOR Decimal Semi-minor axis (meters)
COURSE Decimal Target course (degrees)
SPEED Decimal Target speed
DEPTH Decimal Target depth
LABEL String Usually "MAJORxMINOR" format

GeoJSON Output:
```json
{
"type": "Feature",
"geometry": {
"type": "Polygon",
"coordinates": [[[...ellipse approximation...]]]
},
"properties": {
"kind": "TMA_POS",
"time": "1995-12-12T05:12:00.000+00:00",
"track_id": "NEL STYLE",
"target_id": "TARGET",
"orientation": 130,
"semi_major_axis": 800,
"semi_minor_axis": 300,
"solution": {
"course": 12,
"speed": 4,
"depth": 100
},
"label": "800x300",
"style": { ... }
}
}
```


TMA_RB (TMA Range/Bearing)

REP Format:
```
;TMA_RB: 951212 052200 "NEL STYLE" S@ 124.5 12000 TRACK_061 NULL 050 12.4 100 Trial label
;TMA_RB: YYMMDD HHMMSS "TRACK" SYMBOL BEARING RANGE TARGET_NAME NULL? COURSE SPEED DEPTH LABEL
```

Notes:

  • Symbol format may differ (S@ prefix seen)
  • NULL field purpose unknown
  • Represents bearing/range fix rather than position fix

GeoJSON Output:
```json
{
"type": "Feature",
"geometry": {
"type": "LineString",
"coordinates": [[ownship_lon, ownship_lat], [target_lon, target_lat]]
},
"properties": {
"kind": "TMA_RB",
"time": "1995-12-12T05:22:00+00:00",
"track_id": "NEL STYLE",
"target_id": "TRACK_061",
"bearing": 124.5,
"range": 12000,
"solution": {
"course": 50,
"speed": 12.4,
"depth": 100
},
"label": "Trial label",
"style": { ... }
}
}
```

Geometry:

  • LineString from ownship to computed target position
  • Target position = ownship + bearing/range

TRACKSPLIT (Track Split Marker)

REP Format:
```
;TRACKSPLIT 951212 050210.000 NEL_STYLE2
;TRACKSPLIT YYMMDD HHMMSS.SSS TRACK_NAME
```

Note: No colon after TRACKSPLIT (unlike other annotations).

GeoJSON Output:
```json
{
"type": "Feature",
"geometry": null,
"properties": {
"kind": "TRACKSPLIT",
"time": "1995-12-12T05:02:10.000+00:00",
"track_id": "NEL_STYLE2"
}
}
```

Purpose: Marks a point where a track splits into multiple tracks. No geometry - purely metadata.


Schema Updates Required

New FeatureKindEnum Values

Add to `shared/schemas/src/linkml/common.yaml`:

```yaml
FeatureKindEnum:
permissible_values:
# ... existing values ...
POLYGON:
description: Multi-vertex closed polygon annotation
POLYLINE:
description: Multi-vertex open line annotation
ELLIPSE:
description: Timed ellipse annotation
TIMETEXT:
description: Text annotation with timestamp
PERIODTEXT:
description: Text annotation with time range
WHEEL:
description: Annular region annotation
DYNAMIC_RECT:
description: Time-varying rectangle
DYNAMIC_CIRCLE:
description: Time-varying circle
DYNAMIC_POLY:
description: Time-varying polygon
SENSOR:
description: Sensor contact annotation
TMA_POS:
description: TMA position fix
TMA_RB:
description: TMA range/bearing
TRACKSPLIT:
description: Track split marker
```

New Schema Classes

Add to `shared/schemas/src/linkml/annotations.yaml`:

  1. PolygonAnnotationProperties - kind, label, vertices (for reconstruction)
  2. EllipseAnnotationProperties - kind, time, center, orientation, semi_major_axis, semi_minor_axis, layer
  3. TimeTextAnnotationProperties - kind, time, text
  4. PeriodTextAnnotationProperties - kind, start_time, end_time, text
  5. WheelAnnotationProperties - kind, time, center, inner_radius, outer_radius
  6. DynamicShapeProperties - kind, name, positions array
  7. SensorAnnotationProperties - kind, time, track_id, bearing, range, sensor_type
  8. TMAAnnotationProperties - kind, time, track_id, target_id, orientation, semi_major_axis, semi_minor_axis, solution
  9. TrackSplitProperties - kind, time, track_id

Implementation Files

Parser Modules (services/io/src/debrief_io/handlers/annotations/)

File New Content
`patterns.py` Add regex patterns for each type
`builders.py` Add builder functions for each type
`parser.py` Wire new types into parse_annotations()
`geometry.py` NEW - Ellipse/wheel generation functions

Test Files (services/io/tests/test_annotations/)

File Content
`test_polygon.py` POLY, POLYLINE tests
`test_temporal.py` ELLIPSE, ELLIPSE2, TIMETEXT, PERIODTEXT, WHEEL tests
`test_dynamic.py` DYNAMIC_RECT, DYNAMIC_CIRCLE, DYNAMIC_POLY tests
`test_sensor.py` SENSOR, SENSOR2 tests
`test_tma.py` TMA_POS, TMA_RB tests
`test_tracksplit.py` TRACKSPLIT tests

Schema Files (shared/schemas/)

File Changes
`src/linkml/common.yaml` Add new FeatureKindEnum values
`src/linkml/annotations.yaml` Add new annotation classes
`src/fixtures/valid/` Add fixtures for each new type
`src/fixtures/invalid/` Add invalid fixtures
`tests/test_golden.py` Update ENTITY_MAP
`scripts/generate.py` Update entity_types list

Test Fixtures

shapes.rep (already in fixtures)

Contains examples of all annotation types. Key lines:

```
;POLY: @ga30 21.9 0 0 N 21.5 0 0 W 22 0 0 N 21.8 0 0 W 22.1 0 0 N 21.5 0 0 W test\npoly
;POLYLINE: @C 21.1 0 0 N 21.5 0 0 W 21.2 0 0 N 21.8 0 0 W 21.3 0 0 N 21.5 0 0 W test\npolyline
;ELLIPSE: @f[LAYER=TUAs] 951212 055200 21.4 0 0 N 21.1 0 0 W 65.0 5000 3000 test ellipse
;WHEEL: @C 951212 050200 21.3 0 0 N 21.5 0 0 W 200 1500 test wheel
;DYNAMIC_RECT: @A "Dynamic A" 951212 051000.000 22 00 0 N 21 00 0 W 21 50 0 N 20 50 0 W dynamic A rect 1
;SENSOR: 951212 051100 "NEL STYLE" @A 22 2 27.78 N 21 1 13.78 W -13.9 12000 Plain Cookie SUBJECT
;TMA_POS: 951212 051200.000 "NEL STYLE" @e 22 12 10.14 N 21 34 27.62 W TARGET 130 800 300 012 4 100 800x300
```


Acceptance Criteria

P2 Types

  • POLY parses to closed Polygon with 3+ vertices
  • POLYLINE parses to LineString with 2+ vertices
  • ELLIPSE parses with timestamp, generates polygon approximation
  • ELLIPSE2 parses with time range
  • TIMETEXT parses with single timestamp
  • PERIODTEXT parses with start/end timestamps
  • WHEEL parses with inner/outer radius, generates polygon with hole

P3 Types

  • DYNAMIC_* types group by quoted name
  • SENSOR parses all fields including quoted track name
  • SENSOR2 handles NULL values
  • TMA_POS generates ellipse geometry
  • TMA_RB generates line from ownship to target
  • TRACKSPLIT parses (no geometry)

Integration

  • All new types integrate with existing parse_annotations()
  • All new types include provenance (source_file, line_number)
  • shapes.rep parses without errors
  • No regression in P1 types (189 existing tests pass)

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions