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) if err != nil { return Location{}, err } else if len(results) < 1 { return Location{}, fmt.Errorf("no results for %q", query) } return results[0], nil } func (m *Maps) Search(ctx context.Context, query string) ([]Location, error) { results, err := m.search(ctx, query) for i := len(results) - 1; i >= 0; i-- { shouldKeep := true if m.around != (Location{}) { convertToMiles := 69.0 a := m.around.Lat - results[i].Lat b := m.around.Lng - results[i].Lng dist := math.Sqrt(a*a + b*b) shouldKeep = dist*convertToMiles < 5.0 } 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 }