Files
turbomaps-er/cmd/maps.go
2026-03-14 22:40:20 -06:00

136 lines
2.9 KiB
Go

package cmd
import (
"context"
"encoding/json"
"fmt"
"log"
"math"
"os"
"path"
"golang.org/x/time/rate"
"googlemaps.github.io/maps"
)
type Maps struct {
around Location
cache Cache
}
func NewMapsOf(ctx context.Context, town string) (*Maps, error) {
m := &Maps{
cache: NewCache(ctx),
}
var err error
m.around, err = m.textSearchOne(ctx, town)
return m, err
}
var rps = 2
var mapsLimit = 200
var mapsLimiter = rate.NewLimiter(rate.Limit(rps), 1)
type Location struct {
Name string
Lat float64
Lng float64
Address string
}
func (m *Maps) textSearchOne(ctx context.Context, query string) (Location, error) {
results, err := m.Search(ctx, query, 1.0)
if err != nil {
return Location{}, err
} else if len(results) < 1 {
return Location{}, fmt.Errorf("no results for %q", query)
}
return results[0], nil
}
var convertToMiles = 69.0
func (m *Maps) Search(ctx context.Context, query string, radius_miles float64) ([]Location, error) {
results, err := m.search(ctx, query)
for i := len(results) - 1; i >= 0; i-- {
shouldKeep := true
if m.around != (Location{}) {
a := m.around.Lat - results[i].Lat
b := m.around.Lng - results[i].Lng
dist := math.Sqrt(a*a + b*b)
shouldKeep = dist*convertToMiles < radius_miles
}
if !shouldKeep {
results = append(results[:i], results[i+1:]...)
}
}
return results, err
}
func (m *Maps) search(ctx context.Context, query string) ([]Location, error) {
cacheK := path.Join(fmt.Sprint(m.around), query)
if b, err := m.cache.Get(ctx, cacheK); err == nil {
var locations []Location
log.Printf("cache hit for %q", cacheK)
if err := json.Unmarshal(b, &locations); err == nil {
return locations, nil
}
}
log.Printf("cache miss for %q", cacheK)
locations := []Location{}
nextToken := ""
for len(locations) < mapsLimit {
results, err := m._textSearch(ctx, query, nextToken)
if err != nil {
return nil, err
}
for _, result := range results.Results {
locations = append(locations, Location{
Name: result.Name,
Lat: result.Geometry.Location.Lat,
Lng: result.Geometry.Location.Lng,
Address: result.FormattedAddress,
})
}
nextToken = results.NextPageToken
if nextToken == "" {
break
}
}
cacheV, _ := json.Marshal(locations)
m.cache.Set(ctx, cacheK, cacheV)
return locations, ctx.Err()
}
func (m *Maps) _textSearch(ctx context.Context, query string, nextToken string) (maps.PlacesSearchResponse, error) {
mapsLimiter.Wait(ctx)
var location *maps.LatLng
radius := uint(250)
if m.around != (Location{}) {
radius = 250
location = &maps.LatLng{
Lat: m.around.Lat,
Lng: m.around.Lng,
}
}
return m.client().TextSearch(ctx, &maps.TextSearchRequest{
Query: query,
Location: location,
Radius: radius,
PageToken: nextToken,
})
}
func (*Maps) client() *maps.Client {
c, err := maps.NewClient(maps.WithAPIKey(os.Getenv("GOOGLE_PLACES_API_KEY")))
if err != nil {
panic(err)
}
return c
}