nidus-sync/model_conversion.go
Eli Ribble 1395e3d3ac Remove old FieldSeeker tables, use v2 generated tables.
This requires a bunch of changes since the types on these tables are
much closer to the underlying types of the Fieldseeker data we are
getting back from the API.

I now need to use proper UUID types everywhere, which means I had to
modify the bob gen config to consistently use google UUID, my UUID
library of choice.

I also had to add the organization_id to all the fieldseeker tables
since we rely on them existing for some of our compound queries.

There were some changes to the API type signatures to get things to
build. I may yet regret those.
2025-12-24 17:58:08 -07:00

409 lines
14 KiB
Go

package main
import (
"errors"
"fmt"
"time"
"github.com/Gleipnir-Technology/nidus-sync/db/models"
"github.com/Gleipnir-Technology/nidus-sync/db/sql"
"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
LatLng h3.LatLng `json:"latlng"`
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"`
//Updated *time.Time `json:"updated"`
Comments string `json:"comments"`
}
type TrapNearby struct {
Counts []*TrapCount
Distance string
ID uuid.UUID
}
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
GeometryX float64 `json:"geometryX"`
GeometryY float64 `json:"geometryY"`
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"`
//Updated *time.Time `json:"updated"`
Comments string `json:"comments"`
}
type Treatment struct {
CadenceDelta time.Duration
Date *time.Time
LocationID uuid.UUID
Notes string
Product string
}
func toTemplateTraps(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.MustGet()] = &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, errors.New(fmt.Sprintf("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.MustGet()]
if !ok {
return results, errors.New(fmt.Sprintf("Failed to find counts for %s", location.TrapLocationGlobalid))
}
trap := TrapNearby{
Counts: counts,
Distance: location.Distance,
ID: location.TrapLocationGlobalid.MustGet(),
}
results = append(results, trap)
}
return results, nil
}
func toTemplateTrapData(trap_data models.FieldseekerTrapdatumSlice) ([]TrapData, error) {
var results []TrapData
for _, r := range trap_data {
geometry, err := getPoint(r.Geometry)
if err != nil {
log.Error().Err(err).Msg("Failed to get geometry for trap data")
continue
}
results = append(results, TrapData{
// Basic Identifiers
OrganizationID: r.OrganizationID,
ObjectID: r.Objectid,
GlobalID: r.Globalid.MustGet(),
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
GeometryX: geometry.Lng,
GeometryY: geometry.Lat,
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(""),
//Updated: r.Updated,
Comments: r.Comments.GetOr(""),
})
}
return results, nil
}
func toTemplateTreatment(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 toTemplateBreedingSource(source *models.FieldseekerPointlocation) *BreedingSourceDetail {
lat_lng, err := getPoint(source.Geometry)
if err != nil {
log.Error().Err(err).Msg("Failed to get point from point location")
return nil
}
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.MustGet(),
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
LatLng: lat_lng,
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(""),
//Updated: source.Updated,
Comments: source.Comments.GetOr(""),
}
}
func getTimeOrNull(v null.Val[time.Time]) *time.Time {
if v.IsNull() {
return nil
}
val := v.MustGet()
return &val
}