diff --git a/api/handler.go b/api/handler.go index 3d9d4e1c..a06857a7 100644 --- a/api/handler.go +++ b/api/handler.go @@ -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 { diff --git a/api/routes.go b/api/routes.go index 799161ed..b8d24c5d 100644 --- a/api/routes.go +++ b/api/routes.go @@ -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") diff --git a/platform/geocode/autocomplete.go b/platform/geocode/autocomplete.go index 65799a90..54440a1a 100644 --- a/platform/geocode/autocomplete.go +++ b/platform/geocode/autocomplete.go @@ -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 diff --git a/platform/geocode/by_gid.go b/platform/geocode/by_gid.go new file mode 100644 index 00000000..a1936502 --- /dev/null +++ b/platform/geocode/by_gid.go @@ -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 +} diff --git a/platform/types/address.go b/platform/types/address.go index 46c3976f..ff41a983 100644 --- a/platform/types/address.go +++ b/platform/types/address.go @@ -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"` diff --git a/resource/geocode.go b/resource/geocode.go index c916fda0..95af1fae 100644 --- a/resource/geocode.go +++ b/resource/geocode.go @@ -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 diff --git a/stadia/response_type.go b/stadia/response_type.go index 3c96bca0..af1e31dd 100644 --- a/stadia/response_type.go +++ b/stadia/response_type.go @@ -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"` }