Add APIs for geocoding and reverse-geocoding
This commit is contained in:
parent
437f87013a
commit
43dce16fbd
7 changed files with 175 additions and 40 deletions
|
|
@ -24,11 +24,12 @@ type ErrorAPI struct {
|
|||
var decoder = schema.NewDecoder()
|
||||
|
||||
type handlerFunctionDelete func(context.Context, *http.Request, platform.User) *nhttp.ErrorWithStatus
|
||||
type handlerFunctionGet[T any] func(context.Context, *http.Request, platform.User, resource.QueryParams) (*T, *nhttp.ErrorWithStatus)
|
||||
type handlerFunctionGet[T any] func(context.Context, *http.Request, resource.QueryParams) (*T, *nhttp.ErrorWithStatus)
|
||||
type handlerFunctionGetAuthenticated[T any] func(context.Context, *http.Request, platform.User, resource.QueryParams) (*T, *nhttp.ErrorWithStatus)
|
||||
type handlerFunctionGetImage func(context.Context, *http.Request, platform.User) (file.Collection, uuid.UUID, *nhttp.ErrorWithStatus)
|
||||
type handlerFunctionGetSlice[T any] func(context.Context, *http.Request, resource.QueryParams) ([]*T, *nhttp.ErrorWithStatus)
|
||||
type handlerFunctionGetSliceAuthenticated[T any] func(context.Context, *http.Request, platform.User, resource.QueryParams) ([]*T, *nhttp.ErrorWithStatus)
|
||||
type handlerFunctionPost[RequestType any] func(context.Context, *http.Request, RequestType) (string, *nhttp.ErrorWithStatus)
|
||||
type handlerFunctionPost[RequestType any, ResponseType any] func(context.Context, *http.Request, RequestType) (ResponseType, *nhttp.ErrorWithStatus)
|
||||
type handlerFunctionPostAuthenticated[RequestType any, ResponseType any] func(context.Context, *http.Request, platform.User, RequestType) (ResponseType, *nhttp.ErrorWithStatus)
|
||||
type handlerFunctionPostFormMultipart[RequestType any, ResponseType any] func(context.Context, *http.Request, RequestType) (*ResponseType, *nhttp.ErrorWithStatus)
|
||||
type handlerFunctionPutAuthenticated[RequestType any] func(context.Context, *http.Request, platform.User, RequestType) (string, *nhttp.ErrorWithStatus)
|
||||
|
|
@ -58,7 +59,7 @@ func authenticatedHandlerGetImage(f handlerFunctionGetImage) http.Handler {
|
|||
})
|
||||
}
|
||||
|
||||
func authenticatedHandlerJSON[T any](f handlerFunctionGet[T]) http.Handler {
|
||||
func authenticatedHandlerJSON[T any](f handlerFunctionGetAuthenticated[T]) http.Handler {
|
||||
return auth.NewEnsureAuth(func(w http.ResponseWriter, r *http.Request, u platform.User) {
|
||||
ctx := r.Context()
|
||||
var body []byte
|
||||
|
|
@ -189,6 +190,31 @@ func authenticatedHandlerPostMultipart[ResponseType any](f handlerFunctionPostAu
|
|||
w.Write(body)
|
||||
})
|
||||
}
|
||||
func handlerJSON[T any](f handlerFunctionGet[T]) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
var body []byte
|
||||
var params resource.QueryParams
|
||||
err := decoder.Decode(¶ms, r.URL.Query())
|
||||
if err != nil {
|
||||
respondErrorStatus(w, nhttp.NewBadRequest("failed to decode query: %w", err))
|
||||
return
|
||||
}
|
||||
resp, e := f(ctx, r, params)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
//log.Info().Str("template", template).Err(e).Msg("handler done")
|
||||
if e != nil {
|
||||
respondErrorStatus(w, e)
|
||||
return
|
||||
}
|
||||
body, err = json.Marshal(resp)
|
||||
if err != nil {
|
||||
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
|
||||
return
|
||||
}
|
||||
w.Write(body)
|
||||
}
|
||||
}
|
||||
func handlerJSONSlice[T any](f handlerFunctionGetSlice[T]) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
|
@ -215,7 +241,7 @@ func handlerJSONSlice[T any](f handlerFunctionGetSlice[T]) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
func handlerJSONPost[RequestType any](f handlerFunctionPost[RequestType]) http.HandlerFunc {
|
||||
func handlerJSONPost[RequestType any, ResponseType any](f handlerFunctionPost[RequestType, ResponseType]) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
req, e := parseRequest[RequestType](r)
|
||||
|
|
@ -224,11 +250,16 @@ func handlerJSONPost[RequestType any](f handlerFunctionPost[RequestType]) http.H
|
|||
return
|
||||
}
|
||||
ctx := r.Context()
|
||||
path, e := f(ctx, r, *req)
|
||||
resp, e := f(ctx, r, *req)
|
||||
if e != nil {
|
||||
return
|
||||
}
|
||||
http.Redirect(w, r, path, http.StatusFound)
|
||||
body, err := json.Marshal(resp)
|
||||
if err != nil {
|
||||
respondErrorStatus(w, nhttp.NewError("failed to marshal json: %w", err))
|
||||
return
|
||||
}
|
||||
w.Write(body)
|
||||
}
|
||||
}
|
||||
func handlerFormPost[RequestType any, ResponseType any](f handlerFunctionPostFormMultipart[RequestType, ResponseType]) http.HandlerFunc {
|
||||
|
|
|
|||
|
|
@ -74,6 +74,8 @@ func AddRoutes(r *mux.Router) {
|
|||
district := resource.District(router)
|
||||
r.Handle("/district", handlerJSONSlice(district.List)).Methods("GET")
|
||||
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/suggestion", handlerJSONSlice(geocode.SuggestionList)).Methods("GET")
|
||||
|
||||
//r.HandleFunc("/district", apiGetDistrict).Methods("GET")
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import (
|
|||
|
||||
type AutocompleteResult struct {
|
||||
Detail string
|
||||
GID string
|
||||
Layer string // 'poi', 'postalcode', 'street',
|
||||
Locality string
|
||||
}
|
||||
|
||||
|
|
@ -29,9 +31,17 @@ func Autocomplete(ctx context.Context, org *models.Organization, address string)
|
|||
log.Error().Str("type", r.Type).Msg("should be handled from Stadia")
|
||||
continue
|
||||
}
|
||||
var locality string
|
||||
if r.Properties.CoarseLocation != nil {
|
||||
locality = *r.Properties.CoarseLocation
|
||||
} else {
|
||||
locality = "???"
|
||||
}
|
||||
result[i] = &AutocompleteResult{
|
||||
Detail: r.Properties.Name,
|
||||
Locality: r.Properties.Locality,
|
||||
GID: r.Properties.GID,
|
||||
Layer: r.Properties.Layer,
|
||||
Locality: locality,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
|
|
|
|||
47
platform/geocode/by_gid.go
Normal file
47
platform/geocode/by_gid.go
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
package geocode
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/h3utils"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/stadia"
|
||||
)
|
||||
|
||||
func ByGID(ctx context.Context, gid string) (*GeocodeResult, error) {
|
||||
req := stadia.RequestGeocodeByGID{
|
||||
GIDs: []string{gid},
|
||||
}
|
||||
resp, err := client.GeocodeByGID(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("geocodebygid: %w", err)
|
||||
}
|
||||
if len(resp.Features) < 1 {
|
||||
return nil, fmt.Errorf("no features in result")
|
||||
}
|
||||
feature := resp.Features[0]
|
||||
location := types.Location{
|
||||
Latitude: feature.Geometry.Coordinates[1],
|
||||
Longitude: feature.Geometry.Coordinates[0],
|
||||
}
|
||||
cell, err := h3utils.GetCell(location.Longitude, location.Latitude, 15)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("latlngtocell: %w", err)
|
||||
}
|
||||
return &GeocodeResult{
|
||||
Address: types.Address{
|
||||
Country: feature.Properties.Context.ISO3166A3,
|
||||
GID: feature.Properties.GID,
|
||||
Locality: feature.Properties.Context.WhosOnFirst.Locality.Name,
|
||||
Number: feature.Properties.AddressComponents.Number,
|
||||
PostalCode: feature.Properties.AddressComponents.PostalCode,
|
||||
Raw: feature.Properties.FormattedAddressLine,
|
||||
Region: feature.Properties.Context.WhosOnFirst.Region.Name,
|
||||
Street: feature.Properties.AddressComponents.Street,
|
||||
Unit: "",
|
||||
},
|
||||
Cell: cell,
|
||||
Location: location,
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import (
|
|||
|
||||
type Address struct {
|
||||
Country string `db:"country" json:"country"`
|
||||
GID string `db:"gid" json:"gid"`
|
||||
Locality string `db:"locality" json:"locality"`
|
||||
Number string `db:"number" json:"number"`
|
||||
PostalCode string `db:"postal_code" json:"postal_code"`
|
||||
|
|
|
|||
|
|
@ -2,19 +2,38 @@ package resource
|
|||
|
||||
import (
|
||||
"context"
|
||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/geocode"
|
||||
"net/http"
|
||||
|
||||
nhttp "github.com/Gleipnir-Technology/nidus-sync/http"
|
||||
ngeocode "github.com/Gleipnir-Technology/nidus-sync/platform/geocode"
|
||||
"github.com/Gleipnir-Technology/nidus-sync/platform/types"
|
||||
"github.com/gorilla/mux"
|
||||
//"github.com/rs/zerolog/log"
|
||||
"github.com/uber/h3-go/v4"
|
||||
)
|
||||
|
||||
type geocodeR struct {
|
||||
router *router
|
||||
}
|
||||
type geocode struct {
|
||||
Address types.Address `json:"address"`
|
||||
Cell h3.Cell `json:"cell"`
|
||||
Location types.Location `json:"location"`
|
||||
}
|
||||
|
||||
func newGeocode(g *ngeocode.GeocodeResult) *geocode {
|
||||
return &geocode{
|
||||
Address: g.Address,
|
||||
Cell: g.Cell,
|
||||
Location: g.Location,
|
||||
}
|
||||
}
|
||||
|
||||
type geocodeSuggestion struct {
|
||||
Detail string `json:"detail"`
|
||||
GID string `json:"gid"`
|
||||
Locality string `json:"locality"`
|
||||
Type string `json:"type"`
|
||||
}
|
||||
|
||||
func Geocode(r *router) *geocodeR {
|
||||
|
|
@ -23,11 +42,30 @@ func Geocode(r *router) *geocodeR {
|
|||
}
|
||||
}
|
||||
|
||||
func (res *geocodeR) ByGID(ctx context.Context, r *http.Request, query QueryParams) (*geocode, *nhttp.ErrorWithStatus) {
|
||||
vars := mux.Vars(r)
|
||||
gid := vars["id"]
|
||||
if gid == "" {
|
||||
return nil, nhttp.NewBadRequest("no id")
|
||||
}
|
||||
g, err := ngeocode.ByGID(ctx, gid)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("bygid: %w", err)
|
||||
}
|
||||
return newGeocode(g), nil
|
||||
}
|
||||
func (res *geocodeR) Reverse(ctx context.Context, r *http.Request, location types.Location) (*geocode, *nhttp.ErrorWithStatus) {
|
||||
g, err := ngeocode.ReverseGeocode(ctx, location)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("reverse: %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")
|
||||
}
|
||||
completions, err := geocode.Autocomplete(ctx, nil, *query.Query)
|
||||
completions, err := ngeocode.Autocomplete(ctx, nil, *query.Query)
|
||||
if err != nil {
|
||||
return nil, nhttp.NewError("geocode: %w", err)
|
||||
}
|
||||
|
|
@ -35,7 +73,9 @@ func (res *geocodeR) SuggestionList(ctx context.Context, r *http.Request, query
|
|||
for i, c := range completions {
|
||||
result[i] = &geocodeSuggestion{
|
||||
Detail: c.Detail,
|
||||
GID: c.GID,
|
||||
Locality: c.Locality,
|
||||
Type: c.Layer,
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
|
|
|
|||
|
|
@ -110,40 +110,44 @@ type GeocodeGeometry struct {
|
|||
|
||||
// GeocodeProperties contains the properties of a geocoding result
|
||||
type GeocodeProperties struct {
|
||||
Addendum map[string]interface{} `json:"addendum,omitempty"`
|
||||
AddressComponents AddressComponents `json:"address_components,omitempty"`
|
||||
Accuracy string `json:"accuracy"` // 'point'
|
||||
CoarseLocation *string `json:"coarse_location"` // 'Riverton, UT, USA'
|
||||
Confidence float64 `json:"confidence"` // 1
|
||||
Context Context `json:"context,omitempty"` // bunch of stuff
|
||||
Country string `json:"country"` // 'United States'
|
||||
CountryA string `json:"country_a"` // 'USA'
|
||||
CountryCode string `json:"country_code"` // 'US'
|
||||
CountryGID string `json:"country_gid"` // 'whosonfirst:country:85633793'
|
||||
County string `json:"county"` // "Tulare County"
|
||||
CountyA string `json:"county_a"` // 'TL'
|
||||
CountyGID string `json:"county_gid"` // 'whosonfirst:county:102082895'
|
||||
GID string `json:"gid"` // 'openaddresses:address:us/ca/tulare-addresses-county:fe9dfab3d45c4550'
|
||||
HouseNumber string `json:"housenumber"` // '1234'
|
||||
ID string `json:"id"` // us/ca/tulare-addresses-county:fe9dfab3d45c4550
|
||||
Label string `json:"label"` // 1234 Main St, Dinuba, CA, USA
|
||||
Layer string `json:"layer"` // 'address'
|
||||
Locality string `json:"locality"` // 'Dinuba'
|
||||
LocalityGID string `json:"locality_gid"` // 'whosonfirst:locality:85922491'
|
||||
MatchType string `json:"match_type"` // 'exact'
|
||||
Name string `json:"name"` // '1234 Main St'
|
||||
PostalCode string `json:"postalcode"` // '93618'
|
||||
Precision string `json:"precision"` // 'centroid'
|
||||
Region string `json:"region"` // 'California'
|
||||
RegionA string `json:"region_a"` // 'CA'
|
||||
RegionGID string `json:"region_gid"` // 'whosonfirst:region:85688637'
|
||||
Source string `json:"source"` // 'openaddresses'
|
||||
SourceID string `json:"source"` // 'us/ca/tulare-addresses-county:fe9dfab3d45c4550'
|
||||
Street string `json:"street"` // 'Main Street'
|
||||
Addendum map[string]interface{} `json:"addendum,omitempty"`
|
||||
AddressComponents AddressComponents `json:"address_components,omitempty"`
|
||||
Accuracy string `json:"accuracy"` // 'point'
|
||||
CoarseLocation *string `json:"coarse_location"` // 'Riverton, UT, USA'
|
||||
Confidence float64 `json:"confidence"` // 1
|
||||
Context Context `json:"context,omitempty"` // bunch of stuff
|
||||
Country string `json:"country"` // 'United States'
|
||||
CountryA string `json:"country_a"` // 'USA'
|
||||
CountryCode string `json:"country_code"` // 'US'
|
||||
CountryGID string `json:"country_gid"` // 'whosonfirst:country:85633793'
|
||||
County string `json:"county"` // "Tulare County"
|
||||
CountyA string `json:"county_a"` // 'TL'
|
||||
CountyGID string `json:"county_gid"` // 'whosonfirst:county:102082895'
|
||||
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'
|
||||
HouseNumber string `json:"housenumber"` // '1234'
|
||||
ID string `json:"id"` // us/ca/tulare-addresses-county:fe9dfab3d45c4550
|
||||
Label string `json:"label"` // 1234 Main St, Dinuba, CA, USA
|
||||
Layer string `json:"layer"` // 'address'
|
||||
Locality string `json:"locality"` // 'Dinuba'
|
||||
LocalityGID string `json:"locality_gid"` // 'whosonfirst:locality:85922491'
|
||||
MatchType string `json:"match_type"` // 'exact'
|
||||
Name string `json:"name"` // '1234 Main St'
|
||||
PostalCode string `json:"postalcode"` // '93618'
|
||||
Precision string `json:"precision"` // 'centroid'
|
||||
Region string `json:"region"` // 'California'
|
||||
RegionA string `json:"region_a"` // 'CA'
|
||||
RegionGID string `json:"region_gid"` // 'whosonfirst:region:85688637'
|
||||
Source string `json:"source"` // 'openaddresses'
|
||||
Sources []GeocodeSource `json:"sources"`
|
||||
SourceID string `json:"source_id"` // 'us/ca/tulare-addresses-county:fe9dfab3d45c4550'
|
||||
Street string `json:"street"` // 'Main Street'
|
||||
}
|
||||
|
||||
// GeocodeSource represents a source of geocoding data
|
||||
type GeocodeSource struct {
|
||||
FixitURL string `json:"fixit_url"`
|
||||
Source string `json:"source"`
|
||||
SourceID string `json:"source_id"`
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue