package tools
import (
"fmt"
"os"
"skraak/utils"
)
type CallsPropagateInput struct {
File string `json:"file"`
FromFilter string `json:"from_filter"`
ToFilter string `json:"to_filter"`
Species string `json:"species"`
}
type CallsPropagateOutput struct {
File string `json:"file"`
FromFilter string `json:"from_filter"`
ToFilter string `json:"to_filter"`
Species string `json:"species"`
TargetsExamined int `json:"targets_examined"`
Propagated int `json:"propagated"`
SkippedNoOverlap int `json:"skipped_no_overlap"`
SkippedConflict int `json:"skipped_conflict"`
Conflicts []PropagateConflict `json:"conflicts,omitempty"`
Changes []PropagateChange `json:"changes,omitempty"`
Error string `json:"error,omitempty"`
}
type PropagateConflict struct {
TargetStart float64 `json:"target_start"`
TargetEnd float64 `json:"target_end"`
TargetCallType string `json:"target_calltype,omitempty"`
SourceChoices []PropagateSourceChoice `json:"source_choices"`
}
type PropagateSourceChoice struct {
Start float64 `json:"start"`
End float64 `json:"end"`
Species string `json:"species"`
CallType string `json:"calltype,omitempty"`
}
type PropagateChange struct {
TargetStart float64 `json:"target_start"`
TargetEnd float64 `json:"target_end"`
PrevSpecies string `json:"prev_species"`
PrevCallType string `json:"prev_calltype,omitempty"`
PrevCertainty int `json:"prev_certainty"`
NewSpecies string `json:"new_species"`
NewCallType string `json:"new_calltype,omitempty"`
NewCertainty int `json:"new_certainty"`
}
// CallsPropagate copies verified classifications (certainty==100) from one filter's
// segments to overlapping unverified segments (certainty==70) of another filter,
// within a single .data file. Only source labels matching --species are considered.
// Propagated target labels are set to certainty=90 and file reviewer is set to "Skraak".
func CallsPropagate(input CallsPropagateInput) (CallsPropagateOutput, error) {
output := CallsPropagateOutput{
File: input.File,
FromFilter: input.FromFilter,
ToFilter: input.ToFilter,
Species: input.Species,
}
if input.File == "" {
output.Error = "--file is required"
return output, fmt.Errorf("%s", output.Error)
}
if input.FromFilter == "" {
output.Error = "--from is required"
return output, fmt.Errorf("%s", output.Error)
}
if input.ToFilter == "" {
output.Error = "--to is required"
return output, fmt.Errorf("%s", output.Error)
}
if input.Species == "" {
output.Error = "--species is required"
return output, fmt.Errorf("%s", output.Error)
}
if input.FromFilter == input.ToFilter {
output.Error = "--from and --to must differ"
return output, fmt.Errorf("%s", output.Error)
}
if _, err := os.Stat(input.File); os.IsNotExist(err) {
output.Error = fmt.Sprintf("file not found: %s", input.File)
return output, fmt.Errorf("%s", output.Error)
}
df, err := utils.ParseDataFile(input.File)
if err != nil {
output.Error = fmt.Sprintf("parse %s: %v", input.File, err)
return output, fmt.Errorf("%s", output.Error)
}
type sourceRef struct {
seg *utils.Segment
label *utils.Label
}
var sources []sourceRef
for _, seg := range df.Segments {
for _, lbl := range seg.Labels {
if lbl.Filter == input.FromFilter && lbl.Species == input.Species && lbl.Certainty == 100 {
sources = append(sources, sourceRef{seg: seg, label: lbl})
break
}
}
}
changed := false
for _, tSeg := range df.Segments {
var toLabel *utils.Label
for _, lbl := range tSeg.Labels {
if lbl.Filter == input.ToFilter && lbl.Certainty == 70 {
toLabel = lbl
break
}
}
if toLabel == nil {
continue
}
output.TargetsExamined++
var overlaps []sourceRef
for _, s := range sources {
if s.seg.StartTime < tSeg.EndTime && tSeg.StartTime < s.seg.EndTime {
overlaps = append(overlaps, s)
}
}
if len(overlaps) == 0 {
output.SkippedNoOverlap++
continue
}
agreedCallType := overlaps[0].label.CallType
conflict := false
for _, s := range overlaps[1:] {
if s.label.CallType != agreedCallType {
conflict = true
break
}
}
if conflict {
output.SkippedConflict++
choices := make([]PropagateSourceChoice, 0, len(overlaps))
for _, s := range overlaps {
choices = append(choices, PropagateSourceChoice{
Start: s.seg.StartTime,
End: s.seg.EndTime,
Species: s.label.Species,
CallType: s.label.CallType,
})
}
output.Conflicts = append(output.Conflicts, PropagateConflict{
TargetStart: tSeg.StartTime,
TargetEnd: tSeg.EndTime,
TargetCallType: toLabel.CallType,
SourceChoices: choices,
})
continue
}
change := PropagateChange{
TargetStart: tSeg.StartTime,
TargetEnd: tSeg.EndTime,
PrevSpecies: toLabel.Species,
PrevCallType: toLabel.CallType,
PrevCertainty: toLabel.Certainty,
NewSpecies: input.Species,
NewCallType: agreedCallType,
NewCertainty: 90,
}
toLabel.Species = input.Species
toLabel.CallType = agreedCallType
toLabel.Certainty = 90
changed = true
output.Propagated++
output.Changes = append(output.Changes, change)
}
if changed {
df.Meta.Reviewer = "Skraak"
if err := df.Write(input.File); err != nil {
output.Error = fmt.Sprintf("write %s: %v", input.File, err)
return output, fmt.Errorf("%s", output.Error)
}
}
return output, nil
}