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:
- Parse all DYNAMIC_RECT entries
- Group by quoted name
- 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`:
- PolygonAnnotationProperties - kind, label, vertices (for reconstruction)
- EllipseAnnotationProperties - kind, time, center, orientation, semi_major_axis, semi_minor_axis, layer
- TimeTextAnnotationProperties - kind, time, text
- PeriodTextAnnotationProperties - kind, start_time, end_time, text
- WheelAnnotationProperties - kind, time, center, inner_radius, outer_radius
- DynamicShapeProperties - kind, name, positions array
- SensorAnnotationProperties - kind, time, track_id, bearing, range, sensor_type
- TMAAnnotationProperties - kind, time, track_id, target_id, orientation, semi_major_axis, semi_minor_axis, solution
- 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
P3 Types
Integration
References
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:
This infrastructure is reused for all remaining types.
Priority Groups
P2 - Common Annotation Types
P3 - Specialized Types
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:
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:
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:
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:
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:
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:
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:
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:
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:
Grouping Logic:
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:
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:
Parsing Strategy:
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:
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:
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:
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`:
Implementation Files
Parser Modules (services/io/src/debrief_io/handlers/annotations/)
Test Files (services/io/tests/test_annotations/)
Schema Files (shared/schemas/)
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
P3 Types
Integration
References