Skip to content

TMA solutions

How an analyst makes a target motion analysis (TMA) solution from a run of bearing cuts, reads its bearing residuals in the Bearings view, drags it on the map with the residuals following, leaves wild cuts out of the RMS, and splits it into legs. How legacy plots' solutions come in. Decisions of the product owner, 24 Oct 2026, unless said otherwise; hidden cuts, independent legs and both bearings of an ambiguous cut, 25 Oct 2026; the Bearings view as legacy's Bearing Residuals (Stacked Dots) laid it out, and the way of fitting a leg it serves, 25 Oct 2026; the cuts, legs and positions in the Outline, the operations in the Tools, and Bearings without lists, following the Outline's selection, 27 Oct 2026 (The Outline and the Tools).

A newcomer learns it from the walkthrough Bearings and TMA (walkthroughs), on the sample's towed array, fitted as legacy's tutorial teaches (product owner, 25 Oct 2026, "fit properly"): a side kept, a first guess, its start moved along the first bearing to the target's range, the first leg sheared to fit, a wild bearing left out, a split at the turn and the second leg fitted the same way, to an RMS of 1.8°, near the bearings' own accuracy. Its buttons choose in the Outline (debrief.chooseInOutline: a cut at a time, a period's cuts and the first guess's form, or a solution's position), and the reader acts in the Tools.

Requirements cited

SRD Requirement
CAP-21 A TMA solution is rooted in a selected period of bearing data, its positions at the sensor's observation times in that period, with live bearing residuals while it is dragged. Solutions generated automatically (SATC) are out of scope.
CAP-22 Bearings and ambiguous bearings, resolved to one side by the analyst: what a solution is rooted in (sensors and bearings).
CAP-01, CAP-28 Tracks and their manipulation; the time window.
PRN-08 Analysts work from bearings over time, as legacy Debrief's Stacked Dots showed them.
SUB-22, SUB-24 Every change is one recorded, undoable step with its parameters.
EXT-14 Legacy plots are read as legacy Debrief read them, and what is not imported is listed.
NFR-04 The drag budget: the map follows the pointer smoothly, and what depends on the drag within 100 ms.
NFR-13 Everything works from the keyboard, and by touch.

Model

  1. A solution is a track. A TMA solution is an ordinary track plotted by dead reckoning (DR), so every track operation works on it unchanged: move, rotate, shear, stretch, split, merge, colour, hide, the Tote. What makes it a solution is its tma tag (TmaSolution, schema 0.0.24): the contact it is a solution of (Contact 1), the sensors whose cuts it was made from, its period, the cut ids it is rooted in, the times its legs start, and the cuts left out of its RMS. Its sensors are always those of the cuts it is rooted in: when a sensor's track is split and its later cuts become another sensor's, the solution names that sensor too. The Outline tags it TMA (in VS Code, TMA · Contact 1).
  2. Rooted by cut ids. A solution remembers which cuts it was made from by their ids, not by sensor and period, so it keeps them when its sensor's track is split (and the later cuts become another sensor's) or merged. Its fixes are at the observation times of the cuts shown on the map, one at each distinct time, in the period chosen; its courses and speeds are those of its DR legs, in knots. It has no logged courses or speeds of its own.
  3. Made in one step, from a run of cuts selected in the Outline (or a sensor, for its cuts in the time window) with TMA solution from cuts… in the Tools, rooted in exactly those cuts, and a first guess, either way (the form opens in Bearings):

    • By form. The range along the first cut's bearing, the course and the speed. Debrief's defaults: 10,000 yd (said in the analyst's length unit: 4.94 nmi), a course across the bearing (the bearing plus 90°), 8 kn. A towed array's unresolved first cut asks which of its two bearings to start on.
    • By two clicks on the map. The first cut's line and the last cut's are drawn on the map; click on the first, then on the last. Each click is taken onto its line (the nearest point on it, from the cut's origin outwards); the course and speed follow from the two points and the cuts' times. A click behind the sensor is on no line, and the map says so. Escape stops.

    The step is named so the History reads it: Contact 1 solution made: 60 Bow sonar cuts 12:24:00Z–13:08:30Z. Its notes say the first guess as given (First guess: 5.00 nmi along 257.5°T from where the first cut was taken, then 347.5°T 6.0 kn), and its record holds the request. The next contact's number is the first not in use. 4. Which cuts count. Every cut of the period on the sensors chosen, except those hidden on the map, as legacy Debrief's default (Only plot visible data; product owner, 25 Oct 2026). A hidden cut in the period is still rooted, so shown again it counts again, with nothing else to do; while hidden it is listed, marked ⊘, its residual given but neither drawn nor counted, and the view says how many (3 hidden cuts left out: they are hidden on the map, and count again once shown.). A legacy plot's Visible="false" cuts come in hidden, so its RMS is legacy's. A solution of hidden cuts alone is refused. Each cut's bearing is measured from where the cut was taken: its sensor's array astern of the ship, or its own origin; a cut whose origin Debrief worked out when its track was split (origin_derived) is counted and marked ◇. A cut outside its host's fixes or the solution's gives no residual, and the view says how many. 5. Residuals. For each rooted cut: the measured bearing less the calculated one, the bearing from the cut's origin to the solution's position at the cut's time (along the geodesic between its fixes, the later fix where two share a time), wrapped to −180° to 180°. A resolved cut is measured by the bearing kept. An unresolved ambiguous cut is measured by whichever of its two bearings is nearer the solution, and the view says so (12 ambiguous cuts not yet resolved: each is measured by the bearing nearer the solution (marked ≈)), so the analyst knows the RMS assumed a side. Its other bearing is drawn too, faint, with its residual (other_residual), and never counted, as legacy drew both, so the analyst can pick a side from the plot (product owner, 25 Oct 2026). The RMS is over the cuts counted: those with a residual, shown on the map, not left out. 6. Left out of the RMS. A wild bearing is left out of the RMS, and counted again, as one step each (Leave out of Contact 1 solution and Count in Contact 1 solution in the Tools, for the cuts selected in the Outline, against the solution Bearings shows, or chosen when they are rooted in several; or Space in the plot): Contact 1 solution: cut at 12:24:30Z left out of the RMS, its notes saying the RMS before and after. Several cuts at once are one step. Only rooted cuts can be left out. A cut left out keeps its residual, drawn as a dotted ring and struck through in the list. 7. Legs. A solution is split into legs, one step: by a position in the Outline (product owner, 27 Oct 2026), Split before making it the first position of a new leg, Split after the last of the earlier leg (Contact 1 solution split into legs before its fix at 12:53:30Z); or at a time, S in the plot at the cut chosen there (a manoeuvre picked in the bearings), or current time (Contact 1 solution split into legs at 12:53:30Z). The legs already there keep their positions: nothing moves. Each leg needs two fixes or more. It stays one track: its legs are periods of it, each starting at a fix (leg_starts). On the map, This leg (beside Whole track and Time window under What changes) drags the leg the analyst clicks on the map, at first the one current time is in, by any of the track drags; the note says which (Contact 1 solution, leg 2 of 3). A split is offered only where the leg there keeps two fixes or more either side, as Debrief accepts it.

    How the join behaves (product owner, 25 Oct 2026). Each leg is independent, as in legacy Debrief. The legs are joined by the stretch from one leg's last fix to the next leg's first. Dragging a leg moves only that leg: the legs before and after it, already fitted, never move. The two joins next to it are solved again: the leg before's last fix takes the course and speed that reach the dragged leg's new first fix, and the dragged leg's last fix takes the course and speed that reach the next leg's first fix, so dead reckoning from the first fix still puts every fix where it is. The map's preview shows the same while the drag goes on. Dragging the whole solution moves every leg together. The step's notes say The other legs stay where they are: the joins either side of the leg are solved again.

    Split track, the ordinary track operation, still splits a solution into two tracks: each is then a solution of its own, rooted in its own cuts. Merge joins solutions back into one, each later solution a leg of it, its cuts and exclusions together; each earlier solution's last fix is aimed at the next one's first fix, as a legacy plot's legs are joined, so merging moves nothing. A solution is merged only with other solutions: merged with a track that is not one, Debrief refuses and says why. 8. Legacy plots' solutions (.dpf, EXT-14). A legacy TMA track comes in as a solution track: its absolute legs as legacy saved them (origin, course, speed); its relative legs converted at import to positions, from where the host's sensor array was at the leg's start plus the saved offset, along the geodesic, and the notes say so, with the most any fix is from where legacy drew it. On legacy's sample plots that is mostly within 0.07 nmi, for two reasons where it is more: midflow2's 0.22 nmi is because its saved positions leave out its sensors' plain arrays, 300 and 400 m astern, which v4 applies, as current legacy Debrief does on load; 2648_tx (0.56 nmi over a 16.6 h leg) and Ambig_tracks3 (0.15 nmi at 60°N) are legacy's flat earth, where v4 works along geodesics. Each fix is worked out by DR from the leg's course and speed; the legs are joined by their joins; a dynamic infill between legs is not a fix of the solution (the join takes its place), said in the notes. It is rooted in its host sensor's cuts within its legs; a solution of absolute legs alone names no sensor, so it is rooted in the cuts of the sensors of the legacy tote's primary track within its period, as legacy Stacked Dots measured it, and the notes say so. TMA tracks are no longer listed as not imported; legacy's TMA solution set (contact ellipses) still is. 9. For agents. debrief/tma_create (writing) makes a solution from sensor_ids and a period (or cut_ids), a contact name, and the first guess (range, course, speed, first_bearing, or first_position and last_position; none for Debrief's defaults). It answers the feature, the notes and its residuals. debrief/tma_residuals (reading) answers a solution's residuals (TmaResiduals: a row per rooted cut, with the other bearing's residual of an unresolved ambiguous cut; the RMS, how many counted, left out, hidden, without a residual, measured by the nearer side, from a derived origin). debrief/tma_exclude_cut leaves cuts out of the RMS or counts them again; debrief/tma_split_leg splits a solution into legs at a time, or with side (before or after) by its fix at that time, or with join joins a leg to the one before; debrief/delete_cuts deletes cuts, the solutions rooted in them dropping them; debrief/set_hidden hides cuts, or shows them, saying which solutions they leave or join again; debrief/resolve_ambiguity keeps a side of the cuts named. Each writing method is one step. Held to schemas/vectors/tma-residuals.json and schemas/vectors/tma-legs.json in both languages.

sequenceDiagram
  actor Analyst
  participant View as Bearings view
  participant Map
  participant Host as Host (VS Code or playground)
  participant Python as debrief/tma_*
  Analyst->>Host: the Outline: a sensor's cuts; Shift+Down to the run's last cut
  Analyst->>Host: the Tools: TMA solution from 60 cuts…
  Host->>View: show_tma, focus {make, cut_ids}: the first guess's form (10,000 yd, across, 8 kn)
  View->>Host: tma_create {cut_ids, range, course, speed}
  Host->>Python: debrief/tma_create
  Python-->>Host: one step: "Contact 1 solution made: 60 Bow sonar cuts 12:24:00Z–13:08:30Z"
  Host->>View: the solution's residuals, RMS
  Analyst->>Map: drag the solution's end (shear)
  loop each frame of the drag
    Map->>Host: tma_preview {feature_id, coordinates}
    Host->>View: preview: residuals worked out again, RMS "(while dragging)"
  end
  Analyst->>Map: let go
  Map->>Host: one step: "Contact 1 solution (DR) sheared: 353.9°T 7.0 kn"
  Host->>Python: debrief/tma_residuals
  Python-->>View: the residuals, exact again

The Bearings view

The view works as legacy Debrief's Bearing Residuals view (its Stacked Dots) does: its orientation and its way of working, not its pixels (product owner, 25 Oct 2026).

Where it is. In VS Code, Bearings is in the panel below the editor, beside the Terminal and Problems, a view container of its own (product owner, 25 Oct 2026; Show bearings on a sensor's or a solution's menu in the Outline, and the Bearings and TMA walkthrough, open it there): wide and short, so its panes sit side by side with room each and the time axis takes the panel's height. In Debrief's side bar, as a seventh view, its panes were about 100 px wide. Drag the panel taller for a longer time axis, or move the view where you like, as with any VS Code view. The controls are on top of the plot at any size (product owner, 26 Oct 2026), except where on top they would leave the plot under 220 px, as VS Code's default bottom panel would (about 126 px measured): then, the view wide, the plot takes the whole height on the left and the title, the toolbar and the status line stack in a column beside it (in a 1440 × 900 window the plot about 218 px tall, each pane about 340 px wide). They go back on top once on top would leave 250 px, so a resize never flickers between the two. The same in the playground. In the playground, right of the map, on top of the right-hand column with about 62% of its height, the walkthroughs beneath it (product owner, 25 Oct 2026), so a walkthrough's words stay in sight while working in Bearings; the map is as wide as before. While a solution is open in Bearings the column widens, to about 40% of the window, so each of two panes is 160 px or more, and the map narrows (to 40% of the window, the one time it keeps less than half from 1280 px); with no solution it goes back, and a column the reader has sized is left as they made it. The walkthroughs collapse to their tab from the tab's menu (Collapse to its tabs), giving Bearings the whole column. Shorter than 640 px, the view drops its key and lays the readout over the panes' names; what does not fit scrolls. Dragged elsewhere (under the map, say) it still works. On a phone, and below 900 px, it is a section of the one column, as tall as its plot needs.

Popping it out onto a second monitor (product owner, 28 Oct 2026), as legacy Debrief's Stacked Dots could be. VS Code moves only editors into windows of their own, never a panel's views, so Pop out (the button in the Bearings view's title bar, or Debrief: Pop out Bearings into a new window in the Command Palette) opens Bearings as an editor tab and at once runs VS Code's own View: Move Editor into New Window on it. Drag that window to the second monitor and maximise it, or make it full screen. Where VS Code cannot open the window (VS Code in a browser, or the move failing), Bearings stays an editor tab, which you can drag out of the window yourself or to another editor group; a notification says so once a session. VS Code places the new window itself: an extension cannot choose where it opens or remember where you put it.

  • One Bearings at a time. While popped out, the panel's view says Bearings is open in its own window, with Bring back and Focus that window (keyboard: Tab to them, Enter). The view's title bar has Bring back too, as does the popped-out editor's, and the Command Palette has both. Close the popped-out tab or its window and Bearings is back in the panel.
  • Linked exactly as in the panel. One controller drives whichever is live, so the popped-out view follows current time both ways, the Outline's and the map's selection, the time window, undo and redo, the cut-off, the theme, and the plot reloaded or changed elsewhere; keeping a side, Select bearings, leaving out and splitting there are the same one step each. The panes shown go with it, there and back. Ctrl+Z and Ctrl+Shift+Z undo and redo the plot's steps in the popped-out window, as on the map. The plot's own zoom starts afresh in the new view.
  • Show bearings, the Outline's menus and the Bearings and TMA walkthrough's buttons show whichever is live: the popped-out tab or window, else the panel.
  • Large. In an editor the controls are on top and the plot takes the rest of the height (the rule above: beside it only under 220 px). Measured in VS Code's suite: the new window as VS Code opens it (1024 × 768), the plot 568 px tall, each of two panes 469 px wide (218 px tall in the default bottom panel); the window filling a 1920 × 1080 screen, as maximised, the plot 880 px tall and each pane 917 px wide, the controls on top.
  • In the playground (product owner, 30 Oct 2026): the same. Pop out on Bearings' title bar opens it in a browser window of its own, the same page as VS Code's, linked the same way; the panel says Bearings is open in its own window, with Bring back and Focus that window, and the window's own title bar has Bring back. Closing the window, or reloading the playground, puts it back in the panel. Drag the window to the second monitor yourself; the browser places it. Where the browser blocks new windows, a notice says to allow pop-ups for the page.
  • After VS Code reloads, Debrief does not reopen the plot (nor the map), so a popped-out Bearings would have nothing to be linked to: the editor VS Code restores is closed and Bearings is in the panel again, ready when a plot is opened. Pop it out again then.

It follows the Outline's selection (product owner, 27 Oct 2026): a TMA solution, one of its legs or one of its positions shows that solution (a position ringed in the plot, and current time moved to it); a sensor, or some of its cuts, shows that sensor's coverage (the last cut ringed); a cut of the sensors the solution shown is rooted in keeps the solution shown, as a dot clicked does; something unrelated (another track, an annotation) keeps what is shown. A one-line title says what is shown: Contact 1 solution · leg 2 of 3 · fix at 10:38:00Z, Towed array on BLUE ONE · 132 cuts, 132 ambiguous. It has no lists of its own: no cut list, and no Sensor or Solution pickers, which wasted space (they were the 24 and 25 Oct design); the Outline lists the cuts, legs and positions, and the Tools do what the list's buttons did.

  • The title, one line, what is shown (above).
  • The toolbar, one row: the panes shown (Bearing, Error, Course/Speed, each a toggle), Select bearings (a toggle, below), Hold (Hold scale), Reset (Reset zoom) and ⋯, which shows the Cut-off, the key, the Course/Speed pane's legend (each track named in its colour) and the notes on what the RMS assumed beneath the row.
  • A sensor's coverage, with no solution shown: every bearing of the sensor shown on the map over its whole coverage in the Bearing pane (an unresolved pair both faint), ownship's course and speed if Course/Speed is on; no Error pane, which needs a solution (132 cuts plotted, 132 ambiguous (both bearings, faint). No solution: no errors.). This is where an analyst resolves a towed array's ambiguity, before any solution: BLUE ONE's legs in the Course/Speed pane beside the bearings, and Select bearings to box the run that is the contact.
  • The plot. Time runs up the left, the latest at the top, as legacy's did. Beside each other, sharing that time axis:

    • Bearing (°): every measured bearing of the solution's sensors shown on the map, over their whole period, not only the solution's (each in the cut's colour; an unresolved pair outside the solution both faint); within the solution, a ring for one measured by the nearer of two bearings, its other bearing faint and never counted, a dotted ring for one left out; the solution's calculated bearings as a line in its colour, only where the solution is, and again as small triangles (▲) drawn over the dots, edged in the background colour, so where the measured bearings lie on the line (a solution fitted) it stays in sight beneath them, as legacy's triangles did. Bearings are unwrapped about the calculated ones, so a contact passing north stays one line: legacy's Centre bearing axis on North (0–360° to ±180°) is never needed.
    • Error (°): each residual, only where the solution is, against a strong zero line. Its background is green while every error counted is within the cut-off, pink otherwise, as legacy's calculateErrorShadeFor shaded it: counted as the RMS counts (a residual of a cut shown on the map, not left out; never an unresolved pair's other bearing). Never colour alone: beside it a mark and words say the same, and how many are beyond (product owner, 25 Oct 2026): ✓ Every error within 3.0°; ✗ 2 of 60 beyond 3.0°, the largest 4.4°; a screen reader hears the same words. Its axis fits the errors counted, zero included: an unresolved pair's other bearing and a cut left out are drawn (faint, or a dotted ring) where they fall inside it, and when some fall outside a note says so beside the cue (3 other bearings off scale, 1 left out off scale), never a dot pinned at an edge. Hold scale widens only for the errors counted. A deliberate departure from legacy: legacy's pass/fail shading counted an unresolved pair's other error too (its ambiguous series was in the shaded plot); here it does not, as the RMS does not. The shades are the theme's (VS Code's for lines added and removed); a high-contrast theme, which has none, borders the pane in its passed or failed colour instead.
    • Course/Speed (product owner, 28 Oct 2026): ownship's and the target's course and speed, each in its own track's colour, as on the map. Ownship (the sensor's host, BLUE ONE) always, with or without a solution: its course and speed as logged at its fixes, or as its fixes imply where nothing is logged. The target (the solution's DR legs) when a solution is shown, a little wider. Course solid on the Course (°) axis above, speed dashed on a Speed axis of its own beneath, in the analyst's speed unit, its ticks to the decimals their step needs. Where the two tracks' colours are too near to tell apart, the target is also marked at each fix, so they differ by more than colour; the legend in ⋯ names each (BLUE ONE (ownship): course solid, speed dashed). The course axis fits the courses shown, unwrapped through north as the bearings are; the speed axis fits the speeds from zero; with nothing to plot the pane says No course or speed to plot rather than draw empty axes. Ownship's line is thinned to 2,000 points, each stretch's least and most course and slowest and fastest speed kept, so no turn or sprint is smoothed away. Off by default: at the side column's 270 to 380 px two panes keep about 120 px each to read dots in, and three would leave 80. Turn it on for a manoeuvre, and to resolve a towed array's ambiguity.

    Which panes show is the analyst's, remembered in this browser (the playground) or as the view's state (VS Code, carried to Bearings popped out and back); one at least always shows. Legs' starts are dashed across every pane, current time a line. A crosshair follows the pointer and a readout above the plot says the time and value under it (12:53:30Z · Error +1.6°). The plot is described for a screen reader (Bearings and residuals of Contact 1 solution, time upward from 12:00:00Z to 17:00:00Z; panes Bearing, Error: RMS 1.6° over 60 cuts. Every error within 5.0°.). - Zoom. Drag a box in any pane: time zooms to its height in every pane, and that pane's values to its width. A double-click, Reset zoom, or 0 undoes it. A zoom stays while the solution is dragged, so the fine tuning is done zoomed in. - Select bearings (product owner, 28 Oct 2026): a toggle (aria-pressed). On, the pointer is a cell cross, the status line says what a box does, and says it again while nothing else is said there (in the same line, so the plot never changes size as it comes and goes; one line, whole in its tooltip), and a box dragged in the Bearing pane resolves instead of zooming (a box in another pane still zooms): the box toggles each cut it takes in (product owner, 28 Oct 2026), all at once, as one step with its provenance, named for what it did: an unresolved cut with a dot inside keeps the side whose dot was boxed, its first bearing or its twin, the faint other dot going (Kept 27 Towed array cuts to starboard, 09:46:00Z–10:13:00Z); a resolved cut whose kept dot is inside is made ambiguous again, both its faint dots back (Made 27 Towed array cuts ambiguous again, 09:46:00Z–10:13:00Z); both in one box, Kept 14 cuts to starboard and made 13 ambiguous again (the period, and the sensor where the name would be too long for the History, in the step's next note). An unresolved cut with dots inside on both sides is left as it was, and the step says so (1 cut boxed on both sides, left as they were.); a cut never ambiguous (one bearing, as a bow sonar's) is untouched; hidden cuts are never boxed (they are not drawn). Nothing to change, no step: the status line says why (Nothing to change: 4 cuts boxed on both sides, left as they were.). The zoom stays as it was, during and after, through undo and redo. It works with a sensor's coverage shown (the usual case, before any solution) and with a solution shown. The result is said in the status line, read aloud. It is the same step the Tools' Keep port, Keep starboard and Make ambiguous again take (debrief/resolve_ambiguity, with other_cut_ids for the cuts boxed on their twin and clear_cut_ids for those made ambiguous again), in both hosts, so agents can ask for it too. Only the dots drawn can be boxed: an unresolved pair's both are drawn inside the Bearing axis, which fits both; one beyond a zoom is outside the box's reach until the zoom is reset. - A dot clicked selects its cut, in the Outline (its sensor opened to it) and on the map, and moves current time to it, as legacy's did; the plot rings it. A click on the solution's calculated line or its course or speed selects the solution's position there, in the Outline too: the solution selected, current time at the fix. A click away from any dot moves current time. - The cue and the RMS, one status line beneath the plot, with the bearings off scale, and what the RMS assumed (ambiguous cuts by the nearer side, cuts without a residual, derived origins, hidden cuts left out). While a solution is dragged it says (while dragging). The line is one line whatever it says, so the plot never changes size as the cue comes and goes or its words change between drags: where the view is too narrow, each part is cut short, the whole line in its tooltip, and the cue still read aloud whole (beside the plot it wraps). - What a box or a key did, said in one line beneath them, read aloud whole and whole in its tooltip where the view is too narrow for it (beside the plot it wraps): a message coming or going never changes the plot's size. The plot keeps its size under a box being dragged: the view resized meanwhile (the window, the panel) is laid out again once the button is let go, so the box takes the times and values it was drawn over. - The first guess's form, beneath the status line when TMA solution from cuts… is chosen in the Tools: for exactly the cuts selected in the Outline, or a sensor's in the time window.

The Outline and the Tools

Decisions of the product owner, 27 Oct 2026.

  1. A TMA solution is a tree in the Outline. It expands to its legs (Leg 2 10:38:00Z–12:00:00Z · 44 fixes · 200.0°T 5.2 kn), and each leg to its positions (fixes: 10:38:00Z 200.0°T 5.2 kn), its course and speed those of its DR legs. Selecting a position selects the solution on the map, shows it in Bearings with the position ringed, and moves current time to it; a leg or a position stands for its solution wherever a feature is wanted (the map, the analysis tools).
  2. A sensor expands to its cuts, under its track: time; bearing, with the other side for an ambiguous pair not yet resolved, marked ≈ as Bearings measures it by the nearer (≈ 236.3° stbd / 142.3° port), or the side kept (236.3° stbd kept); its range if it has one; hidden on map if hidden, the row greyed. A screen reader hears Cut at 09:46:00Z, 236.3° to starboard or 142.3° to port, not yet resolved. Selecting cuts selects them on the map; click, Shift-click a range, Ctrl-click to add or take away.
  3. Thousands of rows. In the playground the tree is virtualised: drawn whole up to 600 rows (a plot's features and a sensor or two of a few hundred cuts), and beyond only the rows in sight and some either side, the rest stood in for by spacers, so a sensor of 10,000 cuts opens and scrolls at once. In VS Code the native tree draws only the rows in sight, and children are made only as a branch opens; a sensor of more than 500 cuts opens to pages of 500 by time (09:00:00Z–09:41:30Z, cuts 1–500 of 1,840).
  4. The Tools' entries for the selection are above the analysis tools in the Tools (playground) and in the Tools view's For the Outline's selection (VS Code, where each is also a command and on the tree's menus). Each is one undoable step with its provenance; each shows when the selection has what it acts on, and when it cannot act it is disabled with the reason beside it:

    Entry For Does Disabled when
    Split before 10:38:00Z one position of a leg The position becomes the first of a new leg (leg_starts); the other legs keep their positions: nothing moves it already starts a leg; a leg would keep fewer than two positions either side; not one position
    Split after 10:37:30Z one position of a leg The position becomes the last of its leg, the next starting the new one it already ends a leg, or is the solution's last; as above
    Split before this cut (10:38:00Z), Split after this cut one cut (product owner, 29 Oct 2026) The cut picks a time, not a fix: the solution Bearings shows (else the only one the cut is rooted in) split as at its position at the cut's time (the first after it, before; the last before it, after) no solution shown or rooted; as above
    Keep port, Keep starboard cuts The bearing on that side of the course where each cut's sensor was, for every cut selected at once (a whole period resolved in one step) none of them has two bearings
    Make ambiguous again cuts Both bearings drawn again none has a side kept
    Leave out of Contact 1 solution, Count in Contact 1 solution cuts The cuts left out of the RMS, or counted again, of the solution Bearings shows if they are rooted in it, else the only one; rooted in several and none shown, the analyst chooses none is rooted in a solution; all left out, or all counted, already
    Hide 12 cuts, Show 12 cuts cuts Off the map and out of every solution's residuals (legacy's Visible=false), or back; the notes say which solutions they leave (Contact 1 solution: 3 of its cuts left out of its residuals until shown again) all hidden, or none hidden, already
    Delete 12 cuts cuts Deleted from the plot; the solutions rooted in them drop those ids, and the notes say so (Contact 1 solution drops the 3 cuts deleted it was rooted in: rooted in 28 cuts now, its fixes where they were). Undo brings them back never (but read-only)
    TMA solution from 60 cuts 12:24:00Z–12:53:30Z… cuts, or a sensor The first guess's form in Bearings, the solution rooted in exactly the cuts selected; with only a sensor selected, its cuts shown on the map in the time window. Two clicks on the map stay, from the form all hidden; fewer than two times

    An earlier revision shown read-only disables them all, saying so. The Outline's toolbar (product owner, 28 Oct 2026) also hides and shows cuts, with tracks, solutions and sensors in the same selection, through the same operation, and its Select children turns a sensor selected into all its cuts, a solution into its legs and a leg into its positions (selection). 5. The plot keeps its actions (Keyboard and touch): a dot clicked selects its cut, now in the Outline too; a dot right-clicked (product owner, 29 Oct 2026), in the Bearing or the Error pane, opens the same entries as the Outline's menu for that cut (or the solution's position, for a calculated bearing or a Course/Speed point), each disabled with its reason, and Select this cut (in VS Code a list at the top of the window, as the map's cut menu; in the playground a menu at the pointer, as the Outline's own, where a cut's or a position's row opens the same entries); Space and Delete leave it out; S splits; 1 and 2 keep a side. The map's cut popover stays. Bearings' own Split, Keep, Leave out and Previous and Next ambiguous buttons are gone: the Tools have them, for one cut or many.

The cut-off is the analyst's setting, like their other preferences: 3° unless they set another, kept in their Debrief configuration (tma_error_cut_off, configuration version 4), never in a plot. Set it under ⋯ in the view, or in VS Code's Settings (debrief.tma.errorCutOff); the two agree, and both take 0.01° to 180°.

Fitting a leg, as legacy's tutorial teaches it

  1. Pin the leg's start on the first bearing. Make the solution with its first guess's range along the first cut's bearing (the form), so the first error is zero. Early in the fit the errors ramp away from it, and the Error pane is pink. Bearings never give range: to try another, move the whole solution along that bearing (Move… with the distance and the bearing's reciprocal, or its Move handle with the lowest error kept on zero), then shear again; the RMS may rise on the move before the shear brings it down.
  2. Drag the leg's end (a shear: the start held) and watch the Error pane. Its axis fits the errors every frame, so as they shrink they still fill the pane: a ramp that tilts one way or the other says which way to drag. Drag until the errors flatten about zero and the pane turns green.
  3. Zoom in for the fine tuning: a box round the leg's errors, then small drags (or the arrow keys on the handle). The zoom stays while dragging. Hold scale keeps the Error axis still if the moving scale distracts; it widens rather than hide an error.
  4. The next leg: split at the manoeuvre (Split before in the Tools, on the position in the Outline where the errors start to run away, or S in the plot at the cut chosen there), choose This leg on the map (the leg current time is in; a click picks another), pin its start on its own first bearing (its Move handle, or Move… with This leg), and shear its end the same way; the leg already fitted stays where it is.
  5. When it is done. The cue counts every error of the solution, so it stays pink until every leg is fitted. With noisy bearings it may never turn green: a 1.5° sonar over a hundred cuts leaves a few beyond a 3° cut-off whatever the fit (on the walkthrough's sample, RED ONE's true track leaves 8 of 101). Aim for an RMS near the bearings' own accuracy and only a handful beyond, the largest not far past the cut-off; one far out is a wild bearing to leave out.

Live residuals while dragging

The map previews a drag as it always has (a drag is one step only when let go). For a solution, each frame's positions go to the Bearings view, which works the residuals out again in the browser (the same geodesic and the same rules as Python, held to the same vectors) at most once a frame. Let go, and Python's residuals for the saved step replace them. A drag put back (Escape) puts the residuals back.

The axes while dragging (product owner, 25 Oct 2026). The Error axis fits the errors every frame, as legacy's auto-resize did (BaseStackedDotsView.wrappedUpdateStackedDots); with Hold scale it stays as it was, and widens on the side an error falls outside, never shrinking until let go. No dot is ever drawn at an edge it is not at: the held axis of the first TMA pull request clamped them there, and that is superseded. The Bearing axis keeps its range during a drag, as legacy's did, widened the same way if the calculated bearings leave it. The Course/Speed pane fits its courses and speeds every frame, the solution's worked out from the dragged fixes with the WGS84 ellipsoid's local radii at each leg's middle latitude (localCourse), measured against geographiclib's geodesic: within 0.0001° and 0.0001% for legs to 10 km at latitudes to 70°, at worst 0.0002° and 0.0006% for a 20 km leg at 75°. A zoom wins over all of them.

A frame does only what changes: the residuals worked out again (each row's cut and time parsed once, before the drag begins, and the rows written again in place), the RMS and the cue, and the plot (its size read once a drag, dots of one kind and colour drawn as one path, a dot where its like is already drawn left out, the crosshair on a canvas of its own). The whole frame is timed. Measured in headless Chromium on a shared four-core machine, sixty frames each, all three panes shown, the sensor's other cuts half as many again and ownship's course 2,000 fixes: 600 cuts, median 3 to 4 ms; 1,000 cuts, 5 to 6 ms; 2,000 cuts, 7 to 11 ms, its 95th percentile 12 to 17 ms; the worst single frame at 2,000 cuts 18 to 30 ms, from the machine's pauses rather than the work (NFR-04's 100 ms either way). The playground's test drags a solution of 600, 1,000 and 2,000 cuts and holds each median within a frame.

Keyboard and touch

The Outline is a tree (role="tree"), one tab stop. Up and Down move the keyboard, Right opens a row or goes into it, Left closes it or goes to its parent; Home, End, Page Up and Page Down as usual; Shift with the arrows extends the selection from where it started (a run of cuts); Enter or Space selects the row, Ctrl with them adds or takes it away, Shift with them selects from the anchor; Ctrl+A selects every row shown. Each row says its level, its place and how many share it (aria-level, aria-posinset, aria-setsize, aria-expanded), kept so while virtualised, as the row with the keyboard is always in the page. In VS Code the native tree does the same. The Tools' entries are buttons (a disabled one stays reachable, aria-disabled, its reason beside it and read with it); in VS Code, items of the Tools view, commands in the Command Palette, and items on the tree's menus (Shift+F10).

The plot is a tab stop too, the keyboard's way to everything the pointer does there:

Key In the plot
Up, Down The next later or earlier cut (time runs up), ringed in each pane, read above the plot and said to a screen reader
Enter Select the cut, in the Outline and on the map
Space, Delete Leave the cut chosen out of the RMS, or count it again (as Leave out and Count in in the Tools), one step
S Split the solution into legs at the cut chosen, one step (where each leg keeps two fixes)
1, 2 Keep an unresolved ambiguous cut's first or second bearing, one step (as Keep port or Keep starboard in the Tools)
+, − Zoom time in or out about the cut chosen
], [ Zoom the Error pane's values in or out
Page Up, Page Down Move a zoom later or earlier
0 Reset the zoom (as a double-click, or Reset zoom)

What the plot's keys do is said in the view's live region as well as drawn: the cut chosen (12:41:30Z, Bearing 263.4°, Error +1.2°) and the zoom (Zoomed: 12:20:00Z to 13:10:00Z). Time zooms in to ten seconds at the least.

Select bearings from the keyboard needs no keys of its own: select the run of cuts in the Outline (a sensor opened, Shift with the arrows), then Keep port or Keep starboard in the Tools, or Make ambiguous again to take a side back, the same step (The Outline and the Tools).

The toolbar's toggles are buttons (aria-pressed; in Windows' forced colours a pressed one is drawn in the system's Highlight); ⋯ is a disclosure (aria-expanded), and Escape closes it and puts the keyboard back on it; the cut-off is a number field. The plot's keys choose, select, leave out, split and keep; the Tools do the same for the Outline's selection. The first guess's form takes the keyboard first, at the range; each field says what it means beneath it as it is typed (Means 5.00 nmi.), and a mistake before anything is sent. Two clicks on the map instead has no keyboard way of its own: the form is the keyboard's. On the map, This leg is a button like Whole track, and the drags are the track's, with their keys (track operations); from the keyboard, the leg dragged is the one current time is in (a position chosen in the Outline moves current time to it), and a click or tap on the solution's line picks another. The forms Move…, Rotate…, Shear… and Stretch… offer This leg under What changes too, the leg current time is in, so a leg is fitted by typing (26 Oct 2026). A tap is a click; a box is dragged with a finger as with a mouse (the plot takes the touch, so the page scrolls from beside it).

Not yet

  • Generated solutions (SATC, or a solution fitted to the bearings): out of scope (CAP-21).
  • Residuals while ownship is dragged: ownship moves the cuts' origins; the residuals are worked out again when the drag is saved, not while it goes.
  • Boxing dots off scale: Select bearings boxes the dots drawn. With a zoom, a pair's other side beyond the zoomed Bearing axis is out of the box's reach until the zoom is reset or moved.
  • Several sensors' cuts in one run: cuts of several sensors can be selected in the Outline and a solution made of them, but Bearings shows one sensor's coverage at a time without a solution (the plot of a solution shows every cut of its sensors).
  • Legacy's TMA solution set (contact ellipses): still listed as not imported.

Reference implementation

  • Model: TmaSolution, TmaRequest in schemas/linkml/features.yaml; schemas/linkml/tma.yaml (the view's rows and the residuals; the sensors' other cuts, ownship's courses and the cut-off it plots with); tma_error_cut_off in schemas/linkml/config.yaml (the analyst's cut-off, debrief_config.tma_error_cut_off and @debrief/config's tmaErrorCutOff); the rules in debrief_schemas.documents (a contact named, the period in order, cuts rooted once, cuts left out only of those rooted, legs in order; a request by form or by clicks, not both).
  • Python: debrief_kernel.tma (residuals, vectorised; the first guess; a click onto a cut's line); debrief_stac.tma and Catalogue.create_tma, tma_residuals, tma_exclude, tma_split_leg, with split_track and merge_tracks; debrief_io.dpf_tma (legacy solutions); debrief_stac.tma.split_at_fix_index, split_at_fix, without_cuts and hidden_notes, with Catalogue.delete_cuts and set_hidden; debrief/tma_create, debrief/tma_residuals, debrief/tma_exclude_cut, debrief/tma_split_leg (with side), debrief/delete_cuts and debrief/set_hidden in debrief_tools.service.
  • Hosts: ts/kernel/src/tma.ts (residuals while dragging, words; the coverage and ownship sent to the view), vscode/webview/stackeddots.ts (the plot's axes, fit and hold, cut-off, zoom, pointing, course and speed, panes), vscode/webview/tmacontrol.ts (the view), vscode/webview/tmamap.ts (two clicks), vscode/webview/trackmap.ts (This leg); vscode/webview/outlinemodel.ts (the Outline's legs, positions and cuts, the Tools' entries, what Bearings shows) and vscode/webview/outlinetree.ts (the tree's rows and keys); vscode/src/views/outline.ts (VS Code's tree) and vscode/src/views/tools.ts (its Tools view); vscode/src/panels/bearingsview.ts (the one controller, for the panel or popped out) and vscode/src/panels/bearingshost.ts (the panel's view, the popped-out editor and its window, the placeholder), and the commands debrief.popOutBearings, debrief.bringBackBearings, debrief.focusBearingsWindow, debrief.tmaCreate, debrief.tmaSplitLeg, debrief.tmaExclude, debrief.showBearings, debrief.chooseInOutline, and the Tools' debrief.splitBeforeFix, debrief.splitAfterFix, debrief.hideCuts, debrief.showCuts, debrief.deleteCuts, debrief.tmaLeaveOut, debrief.tmaCountIn, debrief.keepPort, debrief.keepStarboard, debrief.makeAmbiguous and debrief.tmaFromCuts; the playground's in vscode/play/host.ts and vscode/play/outline.ts.
  • Tests: python/debrief-kernel/tests/test_tma.py, python/debrief-stac/tests/test_tma.py, python/debrief-io/tests/test_dpf_tma.py, python/debrief-tools/tests/test_tma_service.py, ts/kernel/test/tma.test.ts, vscode/test/unit/stackeddots.test.ts, vscode/test/unit/outline.test.ts, vscode/test/play/tma.mjs, and two steps of vscode/test/integration/suite.ts; vectors schemas/vectors/tma-residuals.json and schemas/vectors/tma-legs.json.