Add initial Stadia maps integration

This commit is contained in:
Eli Ribble 2026-02-14 15:40:12 +00:00
parent 2bc0e18b9e
commit 427b60132a
No known key found for this signature in database
8 changed files with 368 additions and 0 deletions

53
stadia/bulk.go Normal file
View 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
}

View 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
}

View 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
View 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
View 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
View 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
View 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()
}

View 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&region=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)
}