From 1a22b9233df8f07f2e1ce0ebe93904b170d9221b Mon Sep 17 00:00:00 2001 From: Eli Ribble Date: Thu, 5 Mar 2026 03:17:45 +0000 Subject: [PATCH] Get much more data about signals to send to the planning dash --- api/signal.go | 104 +++++++++++++++++++++++++++++++----- api/types.go | 2 - platform/csv/csv.go | 15 ++++-- platform/geocode/geocode.go | 25 ++++++++- 4 files changed, 126 insertions(+), 20 deletions(-) diff --git a/api/signal.go b/api/signal.go index 15ac79a9..c84d4750 100644 --- a/api/signal.go +++ b/api/signal.go @@ -5,21 +5,36 @@ import ( "net/http" "time" + "github.com/Gleipnir-Technology/bob" + "github.com/Gleipnir-Technology/bob/dialect/psql" "github.com/Gleipnir-Technology/bob/dialect/psql/sm" "github.com/Gleipnir-Technology/nidus-sync/db" "github.com/Gleipnir-Technology/nidus-sync/db/models" nhttp "github.com/Gleipnir-Technology/nidus-sync/http" "github.com/Gleipnir-Technology/nidus-sync/platform" - "github.com/aarondl/opt/null" + //"github.com/aarondl/opt/null" + "github.com/stephenafamo/scan" ) +type Address struct { + Country string `db:"country"` + Locality string `db:"locality"` + Number string `db:"number"` + PostalCode string `db:"postal_code"` + Region string `db:"region"` + Street string `db:"street"` + Unit string `db:"unit"` +} type signal struct { + Address Address Addressed *time.Time `json:"addressed"` Addressor *platform.User `json:"addressed"` Created time.Time `json:"created"` Creator platform.User `json:"creator"` ID int32 `json:"id"` + Location Location `json:"location"` Species string `json:"species"` + Title string `json:"title"` Type string `json:"type"` } type contentListSignal struct { @@ -27,10 +42,68 @@ type contentListSignal struct { } func listSignal(ctx context.Context, r *http.Request, org *models.Organization, user *models.User) (*contentListSignal, *nhttp.ErrorWithStatus) { - rows, err := models.Signals.Query( - models.SelectWhere.Signals.OrganizationID.EQ(org.ID), - sm.OrderBy("created").Desc(), - ).All(ctx, db.PGInstance.BobDB) + type _Row struct { + Address Address + Addressed *time.Time `db:"addressed"` + Addressor *int32 `db:"addressor"` + Created time.Time `db:"created"` + Creator int32 `db:"creator"` + ID int32 `db:"id"` + Latitude float64 `db:"latitude"` + Longitude float64 `db:"longitude"` + Location Location `db:"location"` + Species *string `db:"species"` + Title string `db:"title"` + Type string `db:"type"` + } + rows, err := bob.All(ctx, db.PGInstance.BobDB, psql.Select( + sm.Columns( + "signal.addressed AS addressed", + "signal.addressor AS addressor", + "signal.created AS created", + "signal.creator_id AS creator_id", + "signal.id AS id", + "signal.species AS species", + "signal.title AS title", + "signal.type_ AS type", + "address.country", + "address.locality", + "address.number_", + "address.postal_code", + "address.region", + "address.street", + "address.unit", + "ST_Y(address.geom) AS latitude", + "ST_X(address.geom) AS longitude", + ), + sm.From("signal"), + sm.InnerJoin("signal_pool").OnEQ( + psql.Quote("signal", "id"), + psql.Quote("signal_pool", "signal_id"), + ), + sm.InnerJoin("pool").OnEQ( + psql.Quote("signal_pool", "pool_id"), + psql.Quote("pool", "id"), + ), + sm.InnerJoin("site").On( + psql.And( + psql.Quote("pool", "site_id").EQ(psql.Quote("site", "id")), + psql.Quote("pool", "site_version").EQ(psql.Quote("site", "version")), + ), + ), + sm.InnerJoin("address").OnEQ( + psql.Quote("site", "address_id"), + psql.Quote("address", "id"), + ), + sm.Where(psql.Quote("signal", "organization_id").EQ(psql.Arg(org.ID))), + ), scan.StructMapper[_Row]()) + + /* + rows, err := models.Signals.Query( + models.SelectWhere.Signals.OrganizationID.EQ(org.ID), + sm.OrderBy("created").Desc(), + ).All(ctx, db.PGInstance.BobDB) + */ if err != nil { return nil, nhttp.NewError("failed to get signals: %w", err) } @@ -41,17 +114,22 @@ func listSignal(ctx context.Context, r *http.Request, org *models.Organization, signals := make([]signal, len(rows)) for i, row := range rows { var species string = "" - if row.Species.IsValue() { - species = row.Species.MustGet().String() + if row.Species != nil { + species = *row.Species } signals[i] = signal{ - Addressed: row.Addressed.Ptr(), + Addressed: row.Addressed, Addressor: userOrNil(users_by_id, row.Addressor), Created: row.Created, Creator: *users_by_id[row.Creator], ID: row.ID, - Species: species, - Type: row.Type.String(), + Location: Location{ + Latitude: row.Latitude, + Longitude: row.Longitude, + }, + Species: species, + Title: row.Title, + Type: row.Type, } } return &contentListSignal{ @@ -59,11 +137,11 @@ func listSignal(ctx context.Context, r *http.Request, org *models.Organization, }, nil } -func userOrNil(usersByID map[int32]*platform.User, id null.Val[int32]) *platform.User { - if id.IsNull() { +func userOrNil(usersByID map[int32]*platform.User, id *int32) *platform.User { + if id == nil { return nil } - u, ok := usersByID[id.MustGet()] + u, ok := usersByID[*id] if !ok { return nil } diff --git a/api/types.go b/api/types.go index cac810a6..fc0cdcc3 100644 --- a/api/types.go +++ b/api/types.go @@ -34,12 +34,10 @@ func NewBounds() Bounds { } } -/* not sure if used type Location struct { Latitude float64 Longitude float64 } -*/ type NoteImagePayload struct { UUID string `json:"uuid"` diff --git a/platform/csv/csv.go b/platform/csv/csv.go index 17751a27..a1f4acaa 100644 --- a/platform/csv/csv.go +++ b/platform/csv/csv.go @@ -65,7 +65,13 @@ func JobCommit(ctx context.Context, file_id int32) error { } address, err := geocode.EnsureAddress(ctx, txn, org, a) if err != nil { - return fmt.Errorf("ensure address: %w", err) + //return fmt.Errorf("ensure address: %w", err) + if address == nil { + log.Warn().Err(err).Msg("ensure address failure") + } else { + log.Warn().Err(err).Int32("address.id", address.ID).Msg("ensure address failure") + } + continue } parcel, err := geocode.GetParcel(ctx, txn, address) if err != nil { @@ -135,13 +141,16 @@ func JobCommit(ctx context.Context, file_id int32) error { if err != nil { return fmt.Errorf("insert signal: %w", err) } - _, err = bob.Exec(ctx, db.PGInstance.BobDB, psql.Insert( - im.Into("signa_pool", "pool_id", "signal_id"), + _, err = bob.Exec(ctx, txn, psql.Insert( + im.Into("signal_pool", "pool_id", "signal_id"), im.Values( psql.Arg(pool.ID), psql.Arg(signal.ID), ), )) + if err != nil { + return fmt.Errorf("insert signal pool: %w", err) + } /* Not sure why SignalPools doesn't have an Insert method _, err = models.SignalPools.Insert(&models.SignalPoolSetter{ diff --git a/platform/geocode/geocode.go b/platform/geocode/geocode.go index edec5d6a..67fe2ee7 100644 --- a/platform/geocode/geocode.go +++ b/platform/geocode/geocode.go @@ -130,10 +130,15 @@ func Geocode(ctx context.Context, org *models.Organization, a Address) (GeocodeR if err != nil { return GeocodeResult{}, fmt.Errorf("client structured geocode failure on %s: %w", a.String(), err) } - if len(resp.Features) > 1 { - return GeocodeResult{}, fmt.Errorf("%s matched more than one location", a.String()) + if len(resp.Features) < 1 { + return GeocodeResult{}, fmt.Errorf("%s matched no locations", a.String()) } feature := resp.Features[0] + if len(resp.Features) > 1 { + if !allFeaturesIdenticalEnough(resp.Features) { + return GeocodeResult{}, fmt.Errorf("%s matched more than one location, and they differ a lot", a.String()) + } + } if feature.Geometry.Type != "Point" { return GeocodeResult{}, fmt.Errorf("wrong type %s from %s", feature.Geometry.Type, a.String()) } @@ -179,6 +184,22 @@ func GetParcel(ctx context.Context, txn bob.Tx, a *models.Address) (*models.Parc } return result, nil } +func allFeaturesIdenticalEnough(features []stadia.GeocodeFeature) bool { + if len(features) < 2 { + return true + } + f := features[0].Properties + for _, feature := range features { + if feature.Properties.CountryCode != f.CountryCode || + feature.Properties.County != f.County || + feature.Properties.HouseNumber != f.HouseNumber || + feature.Properties.Locality != f.Locality || + feature.Properties.RegionA != f.RegionA { + return false + } + } + return true +} func maybeAddServiceArea(req *stadia.StructuredGeocodeRequest, org *models.Organization) { if org.ServiceAreaXmax.IsNull() || org.ServiceAreaYmax.IsNull() ||