Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Correct content of SPIKED response composition #489

Merged
merged 1 commit into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/application/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func (a *Application) composeCall(ctx context.Context, call any, out chan<- Mess
response = a.composer.ComposeShoppingResponse(c)
case brevity.SnaplockResponse:
response = a.composer.ComposeSnaplockResponse(c)
case brevity.SpikedResponse:
case brevity.SpikedResponseV2:
response = a.composer.ComposeSpikedResponse(c)
case brevity.TripwireResponse:
response = a.composer.ComposeTripwireResponse(c)
Expand Down
4 changes: 4 additions & 0 deletions pkg/brevity/aspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,7 @@ func AspectFromAngle(bearing bearings.Bearing, track bearings.Bearing) Aspect {
return UnknownAspect
}
}

func (a Aspect) IsCardinal() bool {
return a == Flank || a == Beam || a == Drag
}
8 changes: 4 additions & 4 deletions pkg/brevity/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@ type Group interface {
// Altitude is the group's highest altitude. This may be zero for BOGEY DOPE, SNAPLOCK, and THREAT calls.
Altitude() unit.Length
// Stacks are the group's altitude STACKS, ordered from highest to lowest in intervals of at least 10,000 feet.
// This may be empty for BOGEY DOPE, SNAPLOCK, and THREAT calls.
// This may be empty for BOGEY DOPE, SNAPLOCK, SPIKED and THREAT calls.
Stacks() []Stack
// Track is the group's track direction. This may be UnknownDirection for BOGEY DOPE, SNAPLOCK, and THREAT calls.
// Track is the group's track direction. This may be UnknownDirection for BOGEY DOPE, SNAPLOCK, SPIKED and THREAT calls.
Track() Track
// Aspect is the group's aspect angle relative to another aircraft. This may be nil for BOGEY DOPE, SNAPLOCK, and some THREAT calls.
// Aspect is the group's aspect angle relative to another aircraft. This may be nil for BOGEY DOPE, SNAPLOCK, SPIKED and some THREAT calls.
Aspect() Aspect
// BRAA is an alternate format for the group's location. This is nil except for BOGEY DOPE, SNAPLOCK, and some THREAT calls.
// BRAA is an alternate format for the group's location. This is nil except for BOGEY DOPE, SNAPLOCK, SPIKED, and some THREAT calls.
BRAA() BRAA
// Declaration of the group's friend or foe status.
Declaration() Declaration
Expand Down
15 changes: 15 additions & 0 deletions pkg/brevity/spiked.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ func (r SpikedRequest) String() string {

// SpikedResponse reports any contacts within ±30 degrees of a reported radar spike.
// Reference: ATP 3-52.4 Chapter V section 13.
//
// Deprecated: Use SpikedResponseV2 instead.
type SpikedResponse struct {
// Callsign of the friendly aircraft calling SPIKED.
Callsign string
Expand All @@ -42,3 +44,16 @@ type SpikedResponse struct {
// Reported spike bearing. This is used if the response did not correlate to a group.
Bearing bearings.Bearing
}

// SpikedResponseV2 reports any contacts within ±30 degrees of a reported radar spike.
// Reference: ATP 3-52.4 Chapter V section 13.
type SpikedResponseV2 struct {
// Callsign of the friendly aircraft calling SPIKED.
Callsign string
// Reported spike bearing. This is used if the response did not correlate to a group.
Bearing bearings.Bearing
// True if the spike was correlated to a contact. False otherwise.
Status bool
// Correleted contact group. If Status is false, this may be nil.
Group Group
}
6 changes: 6 additions & 0 deletions pkg/composer/composer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
package composer

import (
"fmt"
"unicode"
)

Expand Down Expand Up @@ -31,6 +32,11 @@ func (r *NaturalLanguageResponse) WriteBoth(s string) {
r.Write(s, s)
}

// WriteBothf appends the formatted string to the subtitle and speech fields.
func (r *NaturalLanguageResponse) WriteBothf(format string, a ...any) {
r.WriteBoth(fmt.Sprintf(format, a...))
}

// WriteResponse appends the given response's subtitle and speech to this response.
func (r *NaturalLanguageResponse) WriteResponse(response NaturalLanguageResponse) {
r.Write(response.Speech, response.Subtitle)
Expand Down
8 changes: 3 additions & 5 deletions pkg/composer/faded.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package composer

import (
"fmt"

"github.com/dharmab/skyeye/pkg/brevity"
)

Expand All @@ -12,7 +10,7 @@ func (c *Composer) ComposeFadedCall(call brevity.FadedCall) (response NaturalLan
if call.Group.Contacts() == 1 {
response.WriteBoth("single contact faded,")
} else {
response.WriteBoth(fmt.Sprintf("%d contacts faded,", call.Group.Contacts()))
response.WriteBothf("%d contacts faded,", call.Group.Contacts())
}

if bullseye := call.Group.Bullseye(); bullseye != nil {
Expand All @@ -21,11 +19,11 @@ func (c *Composer) ComposeFadedCall(call brevity.FadedCall) (response NaturalLan
}

if call.Group.Track() != brevity.UnknownDirection {
response.WriteBoth(fmt.Sprintf(", track %s", call.Group.Track()))
response.WriteBothf(", track %s", call.Group.Track())
}

if call.Group.Declaration() != brevity.Unable {
response.WriteBoth(fmt.Sprintf(", %s", call.Group.Declaration()))
response.WriteBothf(", %s", call.Group.Declaration())
}

for _, platform := range call.Group.Platforms() {
Expand Down
32 changes: 22 additions & 10 deletions pkg/composer/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package composer
import (
"fmt"
"math"
"slices"
"strconv"
"strings"

Expand Down Expand Up @@ -59,31 +58,44 @@ func (c *Composer) composeGroup(group brevity.Group) (response NaturalLanguageRe
fmt.Sprintf("%s %s, %s", label, bullseye.Subtitle, altitude),
)
if isTrackKnown {
response.WriteBoth(fmt.Sprintf(", track %s", group.Track()))
response.WriteBothf(", track %s", group.Track())
}
} else if group.BRAA() != nil {
braa := c.composeBRAA(group.BRAA(), group.Declaration())
response.Write(
fmt.Sprintf("%s %s", label, braa.Speech),
fmt.Sprintf("%s %s", label, braa.Subtitle),
)
isCardinalAspect := slices.Contains([]brevity.Aspect{brevity.Flank, brevity.Beam, brevity.Drag}, group.BRAA().Aspect())
if isCardinalAspect && isTrackKnown {
response.WriteBoth(fmt.Sprintf(" %s", group.Track()))
if group.BRAA().Aspect().IsCardinal() && isTrackKnown {
response.WriteBothf(" %s", group.Track())
}
}

// Declaration
response.WriteBoth(fmt.Sprintf(", %s", group.Declaration()))
response.WriteBoth(", ")
declaration := c.composeDeclaration(group)
response.WriteResponse(declaration)

// Fill-in information
fillIns := c.composeFillIns(group)
response.WriteResponse(fillIns)

response.WriteBoth(".")
return
}

func (*Composer) composeDeclaration(group brevity.Group) (response NaturalLanguageResponse) {
response.WriteBoth(string(group.Declaration()))
if group.MergedWith() == 1 {
response.WriteBoth(", merged with 1 friendly")
}
if group.MergedWith() > 1 {
response.WriteBoth(fmt.Sprintf(", merged with %d friendlies", group.MergedWith()))
response.WriteBothf(", merged with %d friendlies", group.MergedWith())
}
return
}

// Fill-in information

func (c *Composer) composeFillIns(group brevity.Group) (response NaturalLanguageResponse) {
isFurball := group.Declaration() == brevity.Furball

if !isFurball {
Expand All @@ -95,6 +107,7 @@ func (c *Composer) composeGroup(group brevity.Group) (response NaturalLanguageRe
response.WriteResponse(contacts)

if !group.High() {
stacks := group.Stacks()
if len(stacks) > 1 {
response.WriteBoth(", " + c.composeAltitudeFillIns(stacks))
}
Expand All @@ -121,7 +134,6 @@ func (c *Composer) composeGroup(group brevity.Group) (response NaturalLanguageRe
}
}

response.WriteBoth(".")
return
}

Expand Down
58 changes: 32 additions & 26 deletions pkg/composer/spiked.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,45 +2,51 @@ package composer

import (
"fmt"
"slices"

"github.com/dharmab/skyeye/pkg/brevity"
)

// ComposeSpikedResponse constructs natural language brevity for responding to a SPIKED call.
func (c *Composer) ComposeSpikedResponse(response brevity.SpikedResponse) NaturalLanguageResponse {
func (c *Composer) ComposeSpikedResponse(response brevity.SpikedResponseV2) NaturalLanguageResponse {
if response.Status {
reply := fmt.Sprintf(
"%s, spike range %d, %s, %s",
c.composeCallsigns(response.Callsign),
int(response.Range.NauticalMiles()),
c.composeAltitude(response.Altitude, brevity.Bogey),
response.Aspect)
isCardinalAspect := slices.Contains([]brevity.Aspect{brevity.Flank, brevity.Beam, brevity.Drag}, response.Aspect)
isTrackKnown := response.Track != brevity.UnknownDirection
if isCardinalAspect && isTrackKnown {
reply = fmt.Sprintf("%s %s", reply, response.Track)
}
reply = fmt.Sprintf("%s, %s", reply, response.Declaration)
if response.Contacts == 1 {
reply += ", single contact."
} else if response.Contacts > 1 {
reply = fmt.Sprintf("%s, %d contacts.", reply, response.Contacts)
nlr := NaturalLanguageResponse{}

callsigns := c.composeCallsigns(response.Callsign)
nlr.WriteBoth(callsigns)

grp := response.Group

_range := int(grp.BRAA().Range().NauticalMiles())
nlr.WriteBothf(", spike range %d", _range)

nlr.WriteBoth(", ")
altitude := c.composeAltitudeStacks(grp.Stacks(), grp.Declaration())
nlr.WriteBoth(altitude)

nlr.WriteBothf(", %s", grp.BRAA().Aspect())

if grp.BRAA().Aspect().IsCardinal() && grp.Track() != brevity.UnknownDirection {
nlr.WriteBothf(" %s", grp.Track())
}
return NaturalLanguageResponse{
Subtitle: reply,
Speech: reply,
declaration := c.composeDeclaration(grp)
nlr.WriteBoth(", ")
nlr.WriteResponse(declaration)

fillIns := c.composeFillIns(grp)
if len(fillIns.Subtitle) > 0 {
nlr.WriteResponse(fillIns)
}
nlr.WriteBoth(".")
return nlr
}
if response.Bearing == nil {
nlr := NaturalLanguageResponse{}
message := fmt.Sprintf("%s, %s", c.composeCallsigns(response.Callsign), brevity.Unable)
return NaturalLanguageResponse{
Subtitle: message,
Speech: message,
}
nlr.WriteBoth(message)
return nlr
}
return NaturalLanguageResponse{
Subtitle: fmt.Sprintf("%s, %s clean %d.", c.composeCallsigns(response.Callsign), c.composeCallsigns(c.Callsign), int(response.Bearing.Degrees())),
Speech: fmt.Sprintf("%s, %s, clean - %s", c.composeCallsigns(response.Callsign), c.composeCallsigns(c.Callsign), pronounceBearing(response.Bearing)),
Speech: fmt.Sprintf("%s, %s, clean %s", c.composeCallsigns(response.Callsign), c.composeCallsigns(c.Callsign), pronounceBearing(response.Bearing)),
}
}
18 changes: 7 additions & 11 deletions pkg/controller/spiked.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,25 +39,21 @@ func (c *Controller) HandleSpiked(ctx context.Context, request *brevity.SpikedRe

if nearestGroup == nil {
logger.Info().Msg("no hostile groups found within spike cone")
c.calls <- NewCall(ctx, brevity.SpikedResponse{
c.calls <- NewCall(ctx, brevity.SpikedResponseV2{
Callsign: foundCallsign,
Status: false,
Bearing: request.Bearing,
})
return
}
nearestGroup.SetDeclaration(brevity.Hostile)

logger = logger.With().Stringer("group", nearestGroup).Logger()
logger.Debug().Msg("hostile group found within spike cone")
c.calls <- NewCall(ctx, brevity.SpikedResponse{
Callsign: foundCallsign,
Status: true,
Bearing: request.Bearing,
Range: nearestGroup.BRAA().Range(),
Altitude: nearestGroup.BRAA().Altitude(),
Aspect: nearestGroup.BRAA().Aspect(),
Track: nearestGroup.Track(),
Declaration: brevity.Hostile,
Contacts: nearestGroup.Contacts(),
c.calls <- NewCall(ctx, brevity.SpikedResponseV2{
Callsign: foundCallsign,
Status: true,
Bearing: request.Bearing,
Group: nearestGroup,
})
}
Loading