Add API for getting just the closest reverse geocoded answer
Because we don't care about anything that is nearby when the user clicks on the map, we just want the closest thing.
This commit is contained in:
parent
b92697b8c8
commit
96498c01bf
6 changed files with 132 additions and 44 deletions
|
|
@ -102,6 +102,7 @@ func AddRoutes(r *mux.Router) {
|
||||||
geocode := resource.Geocode(router)
|
geocode := resource.Geocode(router)
|
||||||
r.Handle("/geocode/by-gid/{id:.*}", handlerJSON(geocode.ByGID)).Methods("GET")
|
r.Handle("/geocode/by-gid/{id:.*}", handlerJSON(geocode.ByGID)).Methods("GET")
|
||||||
r.Handle("/geocode/reverse", handlerJSONPost(geocode.Reverse)).Methods("POST")
|
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")
|
r.Handle("/geocode/suggestion", handlerJSONSlice(geocode.SuggestionList)).Methods("GET")
|
||||||
publicreport := resource.Publicreport(router)
|
publicreport := resource.Publicreport(router)
|
||||||
r.Handle("/publicreport/{id}", handlerBasic(publicreport.ByID)).Methods("GET").Name("publicreport.ByIDGet")
|
r.Handle("/publicreport/{id}", handlerBasic(publicreport.ByID)).Methods("GET").Name("publicreport.ByIDGet")
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sort"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/Gleipnir-Technology/bob"
|
"github.com/Gleipnir-Technology/bob"
|
||||||
|
|
@ -79,7 +80,7 @@ func GeocodeRaw(ctx context.Context, org *models.Organization, address string) (
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("insert addresses: %w", err)
|
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) {
|
func GeocodeStructured(ctx context.Context, org *models.Organization, a types.Address) (*GeocodeResult, error) {
|
||||||
street := fmt.Sprintf("%s %s", a.Number, a.Street)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("insert addresses: %w", err)
|
return nil, fmt.Errorf("insert addresses: %w", err)
|
||||||
}
|
}
|
||||||
return toGeocodeResult(*resp, a.String(), addresses)
|
return toGeocodeResult(resp.Features, 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the parcel for a given address, if one can be found
|
// 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
|
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 {
|
func allFeaturesIdenticalEnough(features []stadia.GeocodeFeature) bool {
|
||||||
if len(features) < 2 {
|
if len(features) < 2 {
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
49
platform/geocode/sort.go
Normal file
49
platform/geocode/sort.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -2,6 +2,7 @@ package types
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
"github.com/Gleipnir-Technology/nidus-sync/db/models"
|
||||||
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
|
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
|
||||||
|
|
@ -36,3 +37,8 @@ func (l Location) GeometryQuery() (string, error) {
|
||||||
func LocationFromFS(pl *models.FieldseekerPointlocation) Location {
|
func LocationFromFS(pl *models.FieldseekerPointlocation) Location {
|
||||||
return 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))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,13 @@ func (res *geocodeR) Reverse(ctx context.Context, r *http.Request, location type
|
||||||
}
|
}
|
||||||
return newGeocode(g), nil
|
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) {
|
func (res *geocodeR) SuggestionList(ctx context.Context, r *http.Request, query QueryParams) ([]*geocodeSuggestion, *nhttp.ErrorWithStatus) {
|
||||||
if query.Query == nil {
|
if query.Query == nil {
|
||||||
return nil, nhttp.NewBadRequest("you must include a query")
|
return nil, nhttp.NewBadRequest("you must include a query")
|
||||||
|
|
|
||||||
|
|
@ -123,6 +123,7 @@ type GeocodeProperties struct {
|
||||||
County string `json:"county"` // "Tulare County"
|
County string `json:"county"` // "Tulare County"
|
||||||
CountyA string `json:"county_a"` // 'TL'
|
CountyA string `json:"county_a"` // 'TL'
|
||||||
CountyGID string `json:"county_gid"` // 'whosonfirst:county:102082895'
|
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'
|
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'
|
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'
|
GID string `json:"gid"` // 'openaddresses:address:us/ca/tulare-addresses-county:fe9dfab3d45c4550'
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue