Add initial Stadia maps integration
This commit is contained in:
parent
2bc0e18b9e
commit
427b60132a
8 changed files with 368 additions and 0 deletions
53
stadia/bulk.go
Normal file
53
stadia/bulk.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package stadia
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type BulkGeocodeQuery interface {
|
||||
endpoint() string
|
||||
}
|
||||
|
||||
// BulkGeocodeRequestItem represents a single request in a bulk geocoding operation
|
||||
type BulkGeocodeRequestItem struct {
|
||||
Endpoint string `json:"endpoint"`
|
||||
Query BulkGeocodeQuery `json:"query"`
|
||||
}
|
||||
|
||||
// BulkGeocodeResponseItem represents a single response in a bulk geocoding operation
|
||||
type BulkGeocodeResponseItem struct {
|
||||
Response *GeocodeResponse `json:"response,omitempty"`
|
||||
Status int `json:"status"`
|
||||
Message string `json:"msg,omitempty"`
|
||||
}
|
||||
|
||||
func (s *StadiaMaps) BulkGeocode(requests []BulkGeocodeQuery) ([]BulkGeocodeResponseItem, error) {
|
||||
// https://docs.stadiamaps.com/geocoding-search-autocomplete/bulk-geocoding-search/
|
||||
// POST 'https://api.stadiamaps.com/geocoding/v1/search/bulk?api_key=YOUR-API-KEY'
|
||||
body := make([]BulkGeocodeRequestItem, 0)
|
||||
for _, r := range requests {
|
||||
body = append(body, BulkGeocodeRequestItem{
|
||||
Endpoint: r.endpoint(),
|
||||
Query: r,
|
||||
})
|
||||
}
|
||||
var results []BulkGeocodeResponseItem
|
||||
|
||||
resp, err := s.client.R().
|
||||
SetBody(body).
|
||||
SetContentType("application/json").
|
||||
SetPathParam("urlBase", s.urlBase).
|
||||
SetQueryParam("api_key", s.APIKey).
|
||||
SetResult(&results).
|
||||
Post("https://{urlBase}/geocoding/v1/search/bulk")
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bulk geocode request: %w", err)
|
||||
}
|
||||
|
||||
if !resp.IsSuccess() {
|
||||
return nil, fmt.Errorf("bulk geocoding request failed with status code: %d", resp.StatusCode())
|
||||
}
|
||||
|
||||
return results, nil
|
||||
}
|
||||
38
stadia/cmd/bulk-geocode/main.go
Normal file
38
stadia/cmd/bulk-geocode/main.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/stadia"
|
||||
)
|
||||
|
||||
func main() {
|
||||
key := os.Getenv("STADIA_MAPS_API_KEY")
|
||||
if key == "" {
|
||||
log.Println("stadia maps api key is empty")
|
||||
os.Exit(1)
|
||||
}
|
||||
client := stadia.NewStadiaMaps(key)
|
||||
requests := make([]stadia.BulkGeocodeQuery, 0)
|
||||
requests = append(requests, stadia.StructuredGeocodeRequest{
|
||||
Address: strPtr("12932 Ave 404"),
|
||||
PostalCode: strPtr("93615"),
|
||||
})
|
||||
requests = append(requests, stadia.StructuredGeocodeRequest{
|
||||
Address: strPtr("1187 N Arno Rd"),
|
||||
PostalCode: strPtr("93618"),
|
||||
})
|
||||
resp, err := client.BulkGeocode(requests)
|
||||
if err != nil {
|
||||
log.Printf("err: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
for _, r := range resp {
|
||||
log.Printf("Status: %s", r.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
30
stadia/cmd/structured-geocode/main.go
Normal file
30
stadia/cmd/structured-geocode/main.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/Gleipnir-Technology/nidus-sync/stadia"
|
||||
)
|
||||
|
||||
func main() {
|
||||
key := os.Getenv("STADIA_MAPS_API_KEY")
|
||||
if key == "" {
|
||||
log.Println("stadia maps api key is empty")
|
||||
os.Exit(1)
|
||||
}
|
||||
client := stadia.NewStadiaMaps(key)
|
||||
resp, err := client.StructuredGeocode(stadia.StructuredGeocodeRequest{
|
||||
Address: strPtr("12932 Ave 404"),
|
||||
PostalCode: strPtr("93615"),
|
||||
})
|
||||
if err != nil {
|
||||
log.Printf("err: %v\n", err)
|
||||
os.Exit(2)
|
||||
}
|
||||
log.Printf("type: %s", resp.Type)
|
||||
}
|
||||
|
||||
func strPtr(s string) *string {
|
||||
return &s
|
||||
}
|
||||
44
stadia/logger.go
Normal file
44
stadia/logger.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Package restyzerolog provides a wrapper for [zerolog.Logger] to be used with resty
|
||||
// See:
|
||||
// - https://resty.dev
|
||||
// - https://pkg.go.dev/github.com/go-resty/resty/v3#Logger
|
||||
package stadia
|
||||
|
||||
import (
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
// Logger is a wrapper for [zerolog.Logger] to be used as logger for resty.
|
||||
// Contains an instance of [zerolog.Logger], which is used to log messages.
|
||||
// Not exported, because it's not necessary to use it directly.
|
||||
type Logger struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
// New creates a new instance of [Logger] with provided [zerolog.Logger].
|
||||
//
|
||||
// Example of wrapping the default global zerolog logger:
|
||||
//
|
||||
// client := resty.New()
|
||||
// client.SetLogger(restyzerolog.New(log.Logger))
|
||||
//
|
||||
// See:
|
||||
//
|
||||
// - https://pkg.go.dev/github.com/rs/zerolog/log#pkg-variables
|
||||
func NewLogger(logger zerolog.Logger) *Logger {
|
||||
return &Logger{
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Logger) Errorf(format string, v ...any) {
|
||||
r.logger.Error().Msgf(format, v...)
|
||||
}
|
||||
|
||||
func (r *Logger) Warnf(format string, v ...any) {
|
||||
r.logger.Warn().Msgf(format, v...)
|
||||
}
|
||||
|
||||
func (r *Logger) Debugf(format string, v ...any) {
|
||||
r.logger.Debug().Msgf(format, v...)
|
||||
}
|
||||
22
stadia/request_type.go
Normal file
22
stadia/request_type.go
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
package stadia
|
||||
|
||||
// FocusPoint represents focus point coordinates
|
||||
type FocusPoint struct {
|
||||
Lat *float64 `url:"focus.point.lat,omitempty"`
|
||||
Lon *float64 `url:"focus.point.lon,omitempty"`
|
||||
}
|
||||
|
||||
// BoundaryRect represents a bounding rectangle
|
||||
type BoundaryRect struct {
|
||||
MinLon *float64 `url:"boundary.rect.min_lon,omitempty"`
|
||||
MaxLon *float64 `url:"boundary.rect.max_lon,omitempty"`
|
||||
MinLat *float64 `url:"boundary.rect.min_lat,omitempty"`
|
||||
MaxLat *float64 `url:"boundary.rect.max_lat,omitempty"`
|
||||
}
|
||||
|
||||
// BoundaryCircle represents a bounding circle
|
||||
type BoundaryCircle struct {
|
||||
Lat *float64 `url:"boundary.circle.lat,omitempty"`
|
||||
Lon *float64 `url:"boundary.circle.lon,omitempty"`
|
||||
Radius *float64 `url:"boundary.circle.radius,omitempty"`
|
||||
}
|
||||
85
stadia/response_type.go
Normal file
85
stadia/response_type.go
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
package stadia
|
||||
|
||||
// GeocodeResponse represents the top-level response from the geocoding API
|
||||
type GeocodeResponse struct {
|
||||
Geocode GeocodeMeta `json:"geocoding"`
|
||||
Type string `json:"type"` // Should be "FeatureCollection"
|
||||
BBox []float64 `json:"bbox"` // [W, S, E, N]
|
||||
Features []GeocodeFeature `json:"features"`
|
||||
}
|
||||
|
||||
// GeocodeMeta contains metadata about the geocoding request
|
||||
type GeocodeMeta struct {
|
||||
Attribution string `json:"attribution"`
|
||||
Query map[string]interface{} `json:"query,omitempty"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
Errors []string `json:"errors,omitempty"` // v1
|
||||
Error string `json:"error,omitempty"` // v2
|
||||
}
|
||||
|
||||
// GeocodeFeature represents a GeoJSON feature in the response
|
||||
type GeocodeFeature struct {
|
||||
Type string `json:"type"` // Should be "Feature"
|
||||
Geometry GeocodeGeometry `json:"geometry"`
|
||||
Properties GeocodeProperties `json:"properties"`
|
||||
}
|
||||
|
||||
// GeocodeGeometry represents the GeoJSON geometry
|
||||
type GeocodeGeometry struct {
|
||||
Type string `json:"type"` // "Point", "Polygon", etc.
|
||||
Coordinates []float64 `json:"coordinates"`
|
||||
}
|
||||
|
||||
// GeocodeProperties contains the properties of a geocoding result
|
||||
type GeocodeProperties struct {
|
||||
GID string `json:"gid"`
|
||||
Layer string `json:"layer"`
|
||||
Sources []GeocodeSource `json:"sources"`
|
||||
Precision string `json:"precision"`
|
||||
Name string `json:"name"`
|
||||
FormattedAddressLines []string `json:"formatted_address_lines"`
|
||||
FormattedAddressLine string `json:"formatted_address_line"`
|
||||
CoarseLocation string `json:"coarse_location"`
|
||||
AddressComponents AddressComponents `json:"address_components,omitempty"`
|
||||
Context GeocodeContext `json:"context,omitempty"`
|
||||
Confidence float64 `json:"confidence,omitempty"`
|
||||
Distance float64 `json:"distance,omitempty"`
|
||||
Addendum map[string]interface{} `json:"addendum,omitempty"`
|
||||
}
|
||||
|
||||
// GeocodeSource represents a source of geocoding data
|
||||
type GeocodeSource struct {
|
||||
Source string `json:"source"`
|
||||
SourceID string `json:"source_id"`
|
||||
}
|
||||
|
||||
// AddressComponents represents the structured components of an address
|
||||
type AddressComponents struct {
|
||||
Number string `json:"number,omitempty"`
|
||||
Street string `json:"street,omitempty"`
|
||||
Unit string `json:"unit,omitempty"`
|
||||
PostalCode string `json:"postal_code,omitempty"`
|
||||
}
|
||||
|
||||
// GeocodeContext represents the geographic context of a result
|
||||
type GeocodeContext struct {
|
||||
WhosOnFirst WhosOnFirstContext `json:"whosonfirst,omitempty"`
|
||||
ISO3166A2 string `json:"iso_3166_a2,omitempty"`
|
||||
ISO3166A3 string `json:"iso_3166_a3,omitempty"`
|
||||
}
|
||||
|
||||
// WhosOnFirstContext contains geographic hierarchy information
|
||||
type WhosOnFirstContext struct {
|
||||
Country *ContextPlace `json:"country,omitempty"`
|
||||
Region *ContextPlace `json:"region,omitempty"`
|
||||
County *ContextPlace `json:"county,omitempty"`
|
||||
Locality *ContextPlace `json:"locality,omitempty"`
|
||||
Neighbourhood *ContextPlace `json:"neighbourhood,omitempty"`
|
||||
Borough *ContextPlace `json:"borough,omitempty"`
|
||||
}
|
||||
|
||||
// ContextPlace represents a place in the geographic hierarchy
|
||||
type ContextPlace struct {
|
||||
GID string `json:"gid"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
28
stadia/stadia.go
Normal file
28
stadia/stadia.go
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
package stadia
|
||||
|
||||
import (
|
||||
"resty.dev/v3"
|
||||
//"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type StadiaMaps struct {
|
||||
APIKey string
|
||||
|
||||
client *resty.Client
|
||||
urlBase string
|
||||
}
|
||||
|
||||
func NewStadiaMaps(api_key string) *StadiaMaps {
|
||||
//logger := NewLogger(log.Logger)
|
||||
//r := resty.New().SetLogger(logger).SetDebug(true)
|
||||
r := resty.New().SetDebug(true)
|
||||
return &StadiaMaps{
|
||||
APIKey: api_key,
|
||||
client: r,
|
||||
urlBase: "api.stadiamaps.com",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StadiaMaps) Close() {
|
||||
s.client.Close()
|
||||
}
|
||||
68
stadia/structured_geocode.go
Normal file
68
stadia/structured_geocode.go
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package stadia
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
|
||||
"github.com/google/go-querystring/query"
|
||||
)
|
||||
|
||||
// StructuredGeocodeRequest represents the query parameters for structured geocoding
|
||||
type StructuredGeocodeRequest struct {
|
||||
// Address components
|
||||
Address *string `url:"address,omitempty" json:"address,omitempty"`
|
||||
Neighbourhood *string `url:"neighbourhood,omitempty" json:"neighbourhood,omitempty"`
|
||||
Borough *string `url:"borough,omitempty" json:"borough,omitempty"`
|
||||
Locality *string `url:"locality,omitempty" json:"locality,omitempty"`
|
||||
County *string `url:"county,omitempty" json:"county,omitempty"`
|
||||
Region *string `url:"region,omitempty" json:"region,omitempty"`
|
||||
PostalCode *string `url:"postalcode,omitempty" json:"postalcode,omitempty"`
|
||||
Country *string `url:"country,omitempty" json:"country,omitempty"`
|
||||
|
||||
// Focus point
|
||||
FocusPoint *FocusPoint `url:",omitempty" json:",omitempty"`
|
||||
|
||||
// Boundary parameters
|
||||
BoundaryRect *BoundaryRect `url:",omitempty" json:",omitempty"`
|
||||
BoundaryCircle *BoundaryCircle `url:",omitempty" json:",omitempty"`
|
||||
BoundaryCountry []string `url:"boundary.country,omitempty,comma" json:"boundary.country,omitempty,comma"`
|
||||
BoundaryGid *string `url:"boundary.gid,omitempty" json:"boundary.gid,omitempty"`
|
||||
|
||||
// Other parameters
|
||||
Layers []string `url:"layers,omitempty,comma" json:"layers,omitempty,comma"`
|
||||
Sources []string `url:"sources,omitempty,comma" json:"sources,omitempty,comma"`
|
||||
Size *int `url:"size,omitempty" json:"size,omitempty"`
|
||||
Lang *string `url:"lang,omitempty" json:"lang,omitempty"`
|
||||
}
|
||||
|
||||
func (s *StadiaMaps) StructuredGeocode(req StructuredGeocodeRequest) (*GeocodeResponse, error) {
|
||||
// https://docs.stadiamaps.com/geocoding-search-autocomplete/structured-search/
|
||||
// curl "https://api.stadiamaps.com/geocoding/v1/search/structured?address=P%C3%B5hja%20pst%2027a®ion=Harju&country=EE&api_key=YOUR-API-KEY"
|
||||
var result GeocodeResponse
|
||||
|
||||
query, err := req.toQueryParams()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("structured geocode query: %w", err)
|
||||
}
|
||||
resp, err := s.client.R().
|
||||
SetQueryParamsFromValues(query).
|
||||
SetResult(&result).
|
||||
SetPathParam("urlBase", s.urlBase).
|
||||
SetQueryParam("api_key", s.APIKey).
|
||||
Get("https://{urlBase}/geocoding/v1/search/structured")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("structured geocoding get: %w", err)
|
||||
}
|
||||
|
||||
if !resp.IsSuccess() {
|
||||
return nil, fmt.Errorf("structude geocoding status: %w", err)
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (sgr StructuredGeocodeRequest) endpoint() string {
|
||||
return "/v1/search/structured"
|
||||
}
|
||||
func (sgr StructuredGeocodeRequest) toQueryParams() (url.Values, error) {
|
||||
return query.Values(sgr)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue