nidus-sync/platform/trap.go

508 lines
16 KiB
Go
Raw Permalink Normal View History

package platform
import (
"fmt"
"time"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/db/sql"
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
"github.com/aarondl/opt/null"
"github.com/google/uuid"
"github.com/rs/zerolog/log"
"github.com/uber/h3-go/v4"
)
type BreedingSourceDetail struct {
// Basic Information
OrganizationID int32 `json:"organizationId"`
Name string `json:"name"`
Description string `json:"description"`
LocationNumber int64 `json:"locationNumber"`
ObjectID int64 `json:"objectId"`
GlobalID uuid.UUID `json:"globalId"`
ExternalID string `json:"externalId"`
// Status Information
Active bool `json:"active"`
DeactivateReason string `json:"deactivateReason"`
SourceStatus string `json:"sourceStatus"`
Priority string `json:"priority"`
ScalarPriority int64 `json:"scalarPriority"`
// Classification
SourceType string `json:"sourceType"`
Habitat string `json:"habitat"`
UseType string `json:"useType"`
WaterOrigin string `json:"waterOrigin"`
Symbology string `json:"symbology"`
// Geographical Data
H3Cell h3.Cell `json:"h3cell"`
Zone string `json:"zone"`
Zone2 string `json:"zone2"`
Jurisdiction string `json:"jurisdiction"`
AccessDescription string `json:"accessDescription"`
// Inspection Data
LarvaeInspectInterval int16 `json:"larvaeInspectInterval"`
LastInspectionDate *time.Time `json:"lastInspectionDate"`
LastInspectionActivity string `json:"lastInspectionActivity"`
LastInspectionActionTaken string `json:"lastInspectionActionTaken"`
LastInspectionAverageLarvae float64 `json:"lastInspectionAverageLarvae"`
LastInspectionAveragePupae float64 `json:"lastInspectionAveragePupae"`
LastInspectionBreeding string `json:"lastInspectionBreeding"`
LastInspectionConditions string `json:"lastInspectionConditions"`
LastInspectionFieldSpecies string `json:"lastInspectionFieldSpecies"`
LastInspectionLifeStages string `json:"lastInspectionLifeStages"`
// Treatment Data
LastTreatmentDate *time.Time `json:"lastTreatmentDate"`
LastTreatmentActivity string `json:"lastTreatmentActivity"`
LastTreatmentProduct string `json:"lastTreatmentProduct"`
LastTreatmentQuantity float64 `json:"lastTreatmentQuantity"`
LastTreatmentQuantityUnit string `json:"lastTreatmentQuantityUnit"`
// Assignment & Schedule
AssignedTechnician string `json:"assignedTechnician"`
NextActionScheduledDate *time.Time `json:"nextActionScheduledDate"`
// Metadata
Created *time.Time `json:"created"`
Creator string `json:"creator"`
EditedAt *time.Time `json:"editedAt"`
Editor string `json:"editor"`
Comments string `json:"comments"`
}
type BreedingSourceSummary struct {
ID uuid.UUID
Type string
LastInspected *time.Time
LastTreated *time.Time
}
type Trap struct {
Active bool
Comments string
Collections []TrapData
Description string
GlobalID uuid.UUID
H3Cell h3.Cell
}
type TrapCount struct {
Ended time.Time
Females int
ID uuid.UUID
Males int
Total int
}
type TrapData struct {
// Basic Identifiers
OrganizationID int32 `json:"organizationId"`
ObjectID int64 `json:"objectId"`
GlobalID uuid.UUID `json:"globalId"`
LocationName string `json:"locationName"`
LocationID uuid.UUID `json:"locationId"`
SRID uuid.UUID `json:"srid"`
Field int64 `json:"field"`
// Trap Information
TrapType string `json:"trapType"`
TrapCondition string `json:"trapCondition"`
TrapActivityType string `json:"trapActivityType"`
TrapNights int16 `json:"trapNights"`
Lure string `json:"lureType"`
// Personnel
FieldTechnician string `json:"fieldTechnician"`
IdentifiedByTechnician string `json:"identifiedByTechnician"`
SortedByTechnician string `json:"sortedByTechnician"`
// Timing
StartDateTime *time.Time `json:"startDateTime"`
EndDateTime *time.Time `json:"endDateTime"`
// Environmental Conditions
AverageTemperature float64 `json:"averageTemperature"`
Rainfall float64 `json:"rainfall"`
WindDirection string `json:"windDirection"`
WindSpeed float64 `json:"windSpeed"`
SiteCondition string `json:"siteCondition"`
// Status and Processing
Processed bool `json:"processed"`
RecordStatus int16 `json:"recordStatus"`
Reviewed bool `json:"reviewed"`
ReviewedBy string `json:"reviewedBy"`
ReviewedDate *time.Time `json:"reviewedDate"`
GatewaySynced bool `json:"gatewaySynced"`
LR bool `json:"laboratoryReported"`
Voltage float64 `json:"voltage"`
// Location Data
H3Cell h3.Cell `json:"h3cell"`
Zone string `json:"zone"`
Zone2 string `json:"zone2"`
// Vector Survey IDs
VectorSurveyTrapDataID string `json:"vectorSurveyTrapDataId"`
VectorSurveyTrapLocationID string `json:"vectorSurveyTrapLocationId"`
// Metadata
Created *time.Time `json:"created"`
Creator string `json:"creator"`
CreatedByUser string `json:"createdByUser"`
CreatedDateAlt *time.Time `json:"createdDateAlt"`
Edited *time.Time `json:"edited"`
Editor string `json:"editor"`
LastEditedDate *time.Time `json:"lastEditedDate"`
LastEditedUser string `json:"lastEditedUser"`
Comments string `json:"comments"`
// Stuff I actually use
Count TrapCount
}
type TrapNearby struct {
Counts []*TrapCount
Distance string
ID uuid.UUID
}
type TrapSummary struct {
2026-01-15 21:00:42 +00:00
Active bool
Comments string
Description string
GlobalID uuid.UUID
}
type Treatment struct {
CadenceDelta time.Duration
Date *time.Time
LocationID uuid.UUID
Notes string
Product string
}
func toTrap(trap *models.FieldseekerTraplocation, trap_data []sql.TrapDataByLocationIDRecentRow, count_slice []sql.TrapCountByLocationIDRow) (result Trap, err error) {
log.Debug().Str("globalid", trap.Globalid.String()).Msg("Working on trap")
cell, err := h3utils.ToCell(trap.H3cell.MustGet())
if err != nil {
return result, fmt.Errorf("Failed to convert h3 cell: %w", err)
}
count_by_trapdata_id := make(map[uuid.UUID]TrapCount, 0)
for _, count := range count_slice {
count_by_trapdata_id[count.TrapdataGlobalid] = TrapCount{
Ended: count.TrapdataEnddate.MustGet(),
Females: int(count.TotalFemales),
Males: int(count.TotalMales),
Total: int(count.Total),
}
}
data_by_id := make(map[uuid.UUID]TrapData, 0)
for _, dt := range trap_data {
if dt.LocID != trap.Globalid {
return result, fmt.Errorf("Bad query")
}
log.Debug().Str("trapdata", dt.Globalid.String()).Msg("Aggregating trapdata")
count, ok := count_by_trapdata_id[dt.Globalid]
if !ok {
count = TrapCount{}
}
data_by_id[dt.Globalid] = TrapData{
Count: count,
EndDateTime: &dt.Enddatetime,
GlobalID: dt.Globalid,
}
}
data := make([]TrapData, 0)
for _, v := range data_by_id {
data = append(data, v)
}
return Trap{
Active: toBool16Or(trap.Active, false),
Comments: trap.Comments.GetOr(""),
Collections: data,
Description: trap.Description.GetOr(""),
GlobalID: trap.Globalid,
H3Cell: cell,
}, nil
}
func toTemplateTrapSummary(traps models.FieldseekerTraplocationSlice) (results []TrapSummary, err error) {
2026-01-15 21:00:42 +00:00
for _, t := range traps {
results = append(results, TrapSummary{
2026-01-15 21:00:42 +00:00
Active: toBool16Or(t.Active, false),
Comments: t.Comments.GetOr(""),
Description: t.Description.GetOr(""),
GlobalID: t.Globalid,
})
}
return results, err
}
func toTemplateTrapsNearby(locations []sql.TrapLocationBySourceIDRow, trap_data []sql.TrapDataByLocationIDRecentRow, counts []sql.TrapCountByLocationIDRow) ([]TrapNearby, error) {
results := make([]TrapNearby, 0)
count_by_trap_data_id := make(map[uuid.UUID]*sql.TrapCountByLocationIDRow)
for _, c := range counts {
count_by_trap_data_id[c.TrapdataGlobalid] = &c
}
counts_by_location_id := make(map[uuid.UUID][]*TrapCount)
for _, td := range trap_data {
c, ok := count_by_trap_data_id[td.Globalid]
if !ok {
return results, fmt.Errorf("Failed to find trap count for %s", td.Globalid)
}
loc_id := td.LocID
count := &TrapCount{
Ended: td.Enddatetime,
Females: int(c.TotalFemales),
ID: td.Globalid,
Males: int(c.TotalMales),
Total: int(c.Total),
}
counts, ok := counts_by_location_id[loc_id]
if !ok {
counts = []*TrapCount{count}
} else {
counts = append(counts, count)
}
counts_by_location_id[loc_id] = counts
}
for _, location := range locations {
counts, ok := counts_by_location_id[location.TrapLocationGlobalid]
if !ok {
return results, fmt.Errorf("Failed to find counts for %s", location.TrapLocationGlobalid)
}
trap := TrapNearby{
Counts: counts,
Distance: location.Distance,
ID: location.TrapLocationGlobalid,
}
results = append(results, trap)
}
return results, nil
}
func toTemplateTrapData(trap_data models.FieldseekerTrapdatumSlice) ([]TrapData, error) {
var results []TrapData
for _, r := range trap_data {
if r.H3cell.IsNull() {
continue
}
cell, err := h3utils.ToCell(r.H3cell.MustGet())
if err != nil {
log.Error().Err(err).Msg("Failed to get location for trap data")
continue
}
results = append(results, TrapData{
// Basic Identifiers
OrganizationID: r.OrganizationID,
ObjectID: r.Objectid,
GlobalID: r.Globalid,
LocationName: r.Locationname.GetOr(""),
LocationID: r.LocID.GetOr(uuid.UUID{}),
SRID: r.Srid.GetOr(uuid.UUID{}),
Field: int64(r.Field.GetOr(0)),
// Trap Information
TrapType: r.Traptype.GetOr(""),
TrapCondition: r.Trapcondition.GetOr(""),
TrapActivityType: r.Trapactivitytype.GetOr(""),
TrapNights: r.Trapnights.GetOr(0),
Lure: r.Lure.GetOr(""),
// Personnel
FieldTechnician: r.Fieldtech.GetOr(""),
IdentifiedByTechnician: r.Idbytech.GetOr(""),
SortedByTechnician: r.Sortbytech.GetOr(""),
// Timing
StartDateTime: getTimeOrNull(r.Startdatetime),
EndDateTime: getTimeOrNull(r.Enddatetime),
// Environmental Conditions
AverageTemperature: r.Avetemp.GetOr(0),
Rainfall: r.Raingauge.GetOr(0),
WindDirection: r.Winddir.GetOr(""),
WindSpeed: r.Windspeed.GetOr(0),
SiteCondition: r.Sitecond.GetOr(""),
// Status and Processing
Processed: fsIntToBool(r.Processed),
RecordStatus: r.Recordstatus.GetOr(0),
Reviewed: fsIntToBool(r.Reviewed),
ReviewedBy: r.Reviewedby.GetOr(""),
ReviewedDate: getTimeOrNull(r.Revieweddate),
GatewaySynced: fsIntToBool(r.Gatewaysync),
LR: fsIntToBool(r.LR),
Voltage: r.Voltage.GetOr(0),
// Location Data
H3Cell: cell,
Zone: r.Zone.GetOr(""),
Zone2: r.Zone2.GetOr(""),
// Vector Survey IDs
VectorSurveyTrapDataID: r.Vectorsurvtrapdataid.GetOr(""),
VectorSurveyTrapLocationID: r.Vectorsurvtraplocationid.GetOr(""),
// Metadata
Created: getTimeOrNull(r.Creationdate),
Creator: r.Creator.GetOr(""),
CreatedByUser: r.CreatedUser.GetOr(""),
CreatedDateAlt: getTimeOrNull(r.CreatedDate),
Edited: getTimeOrNull(r.Editdate),
Editor: r.Editor.GetOr(""),
LastEditedDate: getTimeOrNull(r.LastEditedDate),
LastEditedUser: r.LastEditedUser.GetOr(""),
Comments: r.Comments.GetOr(""),
})
}
return results, nil
}
func toTreatment(rows models.FieldseekerTreatmentSlice) ([]Treatment, error) {
var results []Treatment
for _, r := range rows {
results = append(results, Treatment{
Date: getTimeOrNull(r.Enddatetime),
LocationID: r.Pointlocid.GetOr(uuid.UUID{}),
Notes: r.Comments.GetOr("none"),
Product: r.Product.GetOr("none"),
})
}
return results, nil
}
func toTemplateInspection(rows models.FieldseekerMosquitoinspectionSlice) ([]Inspection, error) {
var results []Inspection
for _, r := range rows {
results = append(results, Inspection{
Action: r.Actiontaken.GetOr("none"),
Date: getTimeOrNull(r.Enddatetime),
Notes: r.Comments.GetOr("none"),
Location: r.Locationname.GetOr("none"),
LocationID: r.Pointlocid.GetOr(uuid.UUID{}),
})
}
return results, nil
}
// Helper function to convert unix timestamp to time.Time
func fsToTime(val null.Val[int64]) time.Time {
v, ok := val.Get()
if !ok {
return time.UnixMilli(0)
}
t := time.UnixMilli(v)
return t
}
// Helper function to convert int16 to bool
func fsIntToBool(val null.Val[int16]) bool {
if !val.IsValue() {
return false
}
b := val.MustGet() != 0
return b
}
// toTemplateBreedingSource transforms the DB model into the display model
func toBreedingSource(source *models.FieldseekerPointlocation) (*BreedingSourceDetail, error) {
if source.H3cell.IsNull() {
return nil, fmt.Errorf("h3 cell is null")
}
cell, err := h3utils.ToCell(source.H3cell.MustGet())
if err != nil {
return nil, fmt.Errorf("Failed to get h3 cell from point location: %w", err)
}
return &BreedingSourceDetail{
// Basic Information
OrganizationID: source.OrganizationID,
Name: source.Name.MustGet(),
Description: source.Description.MustGet(),
LocationNumber: int64(source.Locationnumber.GetOr(0)),
ObjectID: source.Objectid,
GlobalID: source.Globalid,
ExternalID: source.Externalid.GetOr(""),
// Status Information
Active: fsIntToBool(source.Active),
DeactivateReason: source.DeactivateReason.GetOr(""),
SourceStatus: source.Sourcestatus.GetOr(""),
Priority: source.Priority.GetOr(""),
ScalarPriority: int64(source.Scalarpriority.GetOr(0)),
// Classification
SourceType: source.Stype.GetOr(""),
Habitat: source.Habitat.GetOr(""),
UseType: source.Usetype.GetOr(""),
WaterOrigin: source.Waterorigin.GetOr(""),
Symbology: source.Symbology.GetOr(""),
// Geographical Data
H3Cell: cell,
Zone: source.Zone.GetOr(""),
Zone2: source.Zone2.GetOr(""),
Jurisdiction: source.Jurisdiction.GetOr(""),
AccessDescription: source.Accessdesc.GetOr(""),
// Inspection Data
LarvaeInspectInterval: source.Larvinspectinterval.GetOr(0),
LastInspectionDate: getTimeOrNull(source.Lastinspectdate),
LastInspectionActivity: source.Lastinspectactivity.GetOr(""),
LastInspectionActionTaken: source.Lastinspectactiontaken.GetOr(""),
LastInspectionAverageLarvae: source.Lastinspectavglarvae.GetOr(0),
LastInspectionAveragePupae: source.Lastinspectavgpupae.GetOr(0),
LastInspectionBreeding: source.Lastinspectbreeding.GetOr(""),
LastInspectionConditions: source.Lastinspectconditions.GetOr(""),
LastInspectionFieldSpecies: source.Lastinspectfieldspecies.GetOr(""),
LastInspectionLifeStages: source.Lastinspectlstages.GetOr(""),
// Treatment Data
LastTreatmentDate: getTimeOrNull(source.Lasttreatdate),
LastTreatmentActivity: source.Lasttreatactivity.GetOr(""),
LastTreatmentProduct: source.Lasttreatproduct.GetOr(""),
LastTreatmentQuantity: source.Lasttreatqty.GetOr(0),
LastTreatmentQuantityUnit: source.Lasttreatqtyunit.GetOr(""),
// Assignment & Schedule
AssignedTechnician: source.Assignedtech.GetOr(""),
NextActionScheduledDate: getTimeOrNull(source.Nextactiondatescheduled),
// Metadata
Created: getTimeOrNull(source.Creationdate),
Creator: source.Creator.GetOr(""),
EditedAt: getTimeOrNull(source.Editdate),
Editor: source.Editor.GetOr(""),
Comments: source.Comments.GetOr(""),
}, nil
}
func getTimeOrNull(v null.Val[time.Time]) *time.Time {
if v.IsNull() {
return nil
}
val := v.MustGet()
return &val
}
2026-01-15 21:00:42 +00:00
func toBool16Or(t null.Val[int16], def bool) bool {
if t.IsNull() {
return def
}
val := t.MustGet()
var b bool
if val == 0 {
b = false
} else {
b = true
}
return b
}