diff --git a/api/routes.go b/api/routes.go index bb9ac89a..cd177baa 100644 --- a/api/routes.go +++ b/api/routes.go @@ -102,6 +102,7 @@ func AddRoutes(r *mux.Router) { geocode := resource.Geocode(router) r.Handle("/geocode/by-gid/{id:.*}", handlerJSON(geocode.ByGID)).Methods("GET") r.Handle("/geocode/reverse", handlerJSONPost(geocode.Reverse)).Methods("POST") + r.Handle("/geocode/reverse/closest", handlerJSONPost(geocode.ReverseClosest)).Methods("POST") r.Handle("/geocode/suggestion", handlerJSONSlice(geocode.SuggestionList)).Methods("GET") publicreport := resource.Publicreport(router) r.Handle("/publicreport/{id}", handlerBasic(publicreport.ByID)).Methods("GET").Name("publicreport.ByIDGet") diff --git a/platform/geocode/geocode.go b/platform/geocode/geocode.go index 4ca083df..1ddf7011 100644 --- a/platform/geocode/geocode.go +++ b/platform/geocode/geocode.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/url" + "sort" "time" "github.com/Gleipnir-Technology/bob" @@ -79,7 +80,7 @@ func GeocodeRaw(ctx context.Context, org *models.Organization, address string) ( if err != nil { return nil, fmt.Errorf("insert addresses: %w", err) } - return toGeocodeResult(*resp, address, addresses) + return toGeocodeResult(resp.Features, address, addresses) } func GeocodeStructured(ctx context.Context, org *models.Organization, a types.Address) (*GeocodeResult, error) { street := fmt.Sprintf("%s %s", a.Number, a.Street) @@ -99,49 +100,7 @@ func GeocodeStructured(ctx context.Context, org *models.Organization, a types.Ad if err != nil { return nil, fmt.Errorf("insert addresses: %w", err) } - return toGeocodeResult(*resp, a.String(), addresses) -} -func ReverseGeocode(ctx context.Context, location types.Location) (*GeocodeResult, error) { - req := stadia.RequestReverseGeocode{ - Latitude: location.Latitude, - Longitude: location.Longitude, - } - resp, err := client.ReverseGeocode(ctx, req) - if err != nil { - return nil, fmt.Errorf("client reverse geocode failure on %s: %w", location.String(), err) - } - addresses, err := insertAddresses(ctx, db.PGInstance.BobDB, resp.Features) - if err != nil { - return nil, fmt.Errorf("insert addresses: %w", err) - } - return toGeocodeResult(*resp, location.String(), addresses) - -} -func toGeocodeResult(resp stadia.GeocodeResponse, address_msg string, addresses []types.Address) (*GeocodeResult, error) { - if len(resp.Features) < 1 { - return nil, fmt.Errorf("%s matched no locations", address_msg) - } - if len(addresses) < 1 { - return nil, fmt.Errorf("no addresses") - } - if len(resp.Features) > 1 { - if !allFeaturesIdenticalEnough(resp.Features) { - return nil, fmt.Errorf("%s matched more than one location, and they differ a lot", address_msg) - } - } - feature := resp.Features[0] - address := addresses[0] - if feature.Geometry.Type != "Point" { - return nil, fmt.Errorf("wrong type %s from %s", feature.Geometry.Type, address_msg) - } - cell, err := h3utils.GetCell(address.Location.Longitude, address.Location.Latitude, 15) - if err != nil { - return nil, fmt.Errorf("failed to convert lat %f lng %f to h3 cell", address.Location.Longitude, address.Location.Latitude) - } - return &GeocodeResult{ - Address: address, - Cell: cell, - }, nil + return toGeocodeResult(resp.Features, a.String(), addresses) } // Get the parcel for a given address, if one can be found @@ -161,6 +120,71 @@ func GetParcel(ctx context.Context, txn bob.Executor, a types.Address) (*models. } return result, nil } +func ReverseGeocode(ctx context.Context, location types.Location) (*GeocodeResult, error) { + req := stadia.RequestReverseGeocode{ + Latitude: location.Latitude, + Longitude: location.Longitude, + } + resp, err := client.ReverseGeocode(ctx, req) + if err != nil { + return nil, fmt.Errorf("client reverse geocode failure on %s: %w", location.String(), err) + } + addresses, err := insertAddresses(ctx, db.PGInstance.BobDB, resp.Features) + if err != nil { + return nil, fmt.Errorf("insert addresses: %w", err) + } + return toGeocodeResult(resp.Features, location.String(), addresses) + +} +func ReverseGeocodeClosest(ctx context.Context, location types.Location) (*GeocodeResult, error) { + req := stadia.RequestReverseGeocode{ + Latitude: location.Latitude, + Longitude: location.Longitude, + } + resp, err := client.ReverseGeocode(ctx, req) + if err != nil { + return nil, fmt.Errorf("client reverse geocode failure on %s: %w", location.String(), err) + } + addresses, err := insertAddresses(ctx, db.PGInstance.BobDB, resp.Features) + if err != nil { + return nil, fmt.Errorf("insert addresses: %w", err) + } + /* + sorter := SortAddressByDistance{ + Addresses: addresses, + Location: location, + } + */ + sort.Sort(SortFeaturesByDistance(resp.Features)) + return toGeocodeResult(resp.Features[:1], location.String(), addresses[:1]) + +} +func toGeocodeResult(features []stadia.GeocodeFeature, address_msg string, addresses []types.Address) (*GeocodeResult, error) { + if len(features) < 1 { + return nil, fmt.Errorf("%s matched no locations", address_msg) + } + if len(addresses) < 1 { + return nil, fmt.Errorf("no addresses") + } + if len(features) > 1 { + if !allFeaturesIdenticalEnough(features) { + return nil, fmt.Errorf("%s matched more than one location, and they differ a lot", address_msg) + } + } + feature := features[0] + address := addresses[0] + if feature.Geometry.Type != "Point" { + return nil, fmt.Errorf("wrong type %s from %s", feature.Geometry.Type, address_msg) + } + cell, err := h3utils.GetCell(address.Location.Longitude, address.Location.Latitude, 15) + if err != nil { + return nil, fmt.Errorf("failed to convert lat %f lng %f to h3 cell", address.Location.Longitude, address.Location.Latitude) + } + return &GeocodeResult{ + Address: address, + Cell: cell, + }, nil +} func allFeaturesIdenticalEnough(features []stadia.GeocodeFeature) bool { if len(features) < 2 { return true diff --git a/platform/geocode/sort.go b/platform/geocode/sort.go new file mode 100644 index 00000000..3c5e394d --- /dev/null +++ b/platform/geocode/sort.go @@ -0,0 +1,49 @@ +package geocode + +import ( + "github.com/Gleipnir-Technology/nidus-sync/platform/types" + "github.com/Gleipnir-Technology/nidus-sync/stadia" +) + +type SortAddressByDistance struct { + Addresses []types.Address + Location types.Location +} + +func (s SortAddressByDistance) Len() int { return len(s.Addresses) } +func (s SortAddressByDistance) Swap(i, j int) { + s.Addresses[i], s.Addresses[j] = s.Addresses[j], s.Addresses[i] +} +func (s SortAddressByDistance) Less(i, j int) bool { + ai := s.Addresses[i] + aj := s.Addresses[j] + if ai.Location == nil || (ai.Location.Latitude == 0 && ai.Location.Longitude == 0) { + if aj.Location == nil || (aj.Location.Latitude == 0 && aj.Location.Longitude == 0) { + return ai.Raw > aj.Raw + } + return false + } else if aj.Location == nil || (aj.Location.Latitude == 0 && aj.Location.Longitude == 0) { + return true + } + di := types.LocationDistance(s.Location, *ai.Location) + dj := types.LocationDistance(s.Location, *ai.Location) + return di < dj +} + +type SortFeaturesByDistance []stadia.GeocodeFeature + +func (s SortFeaturesByDistance) Len() int { return len(s) } +func (s SortFeaturesByDistance) Swap(i, j int) { s[i], s[j] = s[j], s[i] } +func (s SortFeaturesByDistance) Less(i, j int) bool { + fi := s[i].Properties.Distance + fj := s[j].Properties.Distance + if fi == nil { + if fj == nil { + return s[i].Properties.GID < s[j].Properties.GID + } + return false + } else if fj == nil { + return true + } + return *fi < *fj +} diff --git a/platform/types/location.go b/platform/types/location.go index 95d73af1..e14a3ca0 100644 --- a/platform/types/location.go +++ b/platform/types/location.go @@ -2,6 +2,7 @@ package types import ( "fmt" + "math" "github.com/Gleipnir-Technology/nidus-sync/db/models" "github.com/Gleipnir-Technology/nidus-sync/h3utils" @@ -36,3 +37,8 @@ func (l Location) GeometryQuery() (string, error) { func LocationFromFS(pl *models.FieldseekerPointlocation) Location { return Location{} } +func LocationDistance(l1 Location, l2 Location) float64 { + lat_delta := l1.Latitude - l2.Latitude + lng_delta := l1.Longitude - l2.Longitude + return math.Sqrt((lat_delta * lat_delta) + (lng_delta * lng_delta)) +} diff --git a/resource/geocode.go b/resource/geocode.go index f15abfe4..bacad4d0 100644 --- a/resource/geocode.go +++ b/resource/geocode.go @@ -59,6 +59,13 @@ func (res *geocodeR) Reverse(ctx context.Context, r *http.Request, location type } return newGeocode(g), nil } +func (res *geocodeR) ReverseClosest(ctx context.Context, r *http.Request, location types.Location) (*geocode, *nhttp.ErrorWithStatus) { + g, err := ngeocode.ReverseGeocodeClosest(ctx, location) + if err != nil { + return nil, nhttp.NewError("reverse closest: %w", err) + } + return newGeocode(g), nil +} func (res *geocodeR) SuggestionList(ctx context.Context, r *http.Request, query QueryParams) ([]*geocodeSuggestion, *nhttp.ErrorWithStatus) { if query.Query == nil { return nil, nhttp.NewBadRequest("you must include a query") diff --git a/stadia/response_type.go b/stadia/response_type.go index 16f114da..aa64626a 100644 --- a/stadia/response_type.go +++ b/stadia/response_type.go @@ -123,6 +123,7 @@ type GeocodeProperties struct { County string `json:"county"` // "Tulare County" CountyA string `json:"county_a"` // 'TL' CountyGID string `json:"county_gid"` // 'whosonfirst:county:102082895' + Distance *float64 `json:"distance"` // FormattedAddressLine string `json:"formatted_address_line"` // '123 Main Street, Riverton, Utah 84065, United States of America' FormattedAddressLines []string `json:"formatted_address_lines"` // '123 Main Street', 'Riverton, Utah 84065', 'United States of America' GID string `json:"gid"` // 'openaddresses:address:us/ca/tulare-addresses-county:fe9dfab3d45c4550'