diff --git a/cmd/.db.go.swp b/cmd/.db.go.swp new file mode 100644 index 0000000..8e69522 Binary files /dev/null and b/cmd/.db.go.swp differ diff --git a/cmd/db.go b/cmd/db.go new file mode 100644 index 0000000..22724b4 --- /dev/null +++ b/cmd/db.go @@ -0,0 +1,69 @@ +package cmd + +import ( + "context" + "database/sql" + "time" + + _ "modernc.org/sqlite" +) + +type DB struct { + *sql.DB +} + +type Cache DB + +var cacheAddr = "/tmp/turbomaps-er.db" + +func NewCache(ctx context.Context) Cache { + ctx, can := context.WithTimeout(ctx, 5*time.Second) + defer can() + + db, err := sql.Open("sqlite", cacheAddr) + if err != nil { + panic(err) + } + + if err := db.PingContext(ctx); err != nil { + panic(err) + } + + return Cache(DB{DB: db}) +} + +func (db Cache) Get(ctx context.Context, k string) ([]byte, error) { + if err := db.init(ctx); err != nil { + return nil, err + } + + row := db.QueryRowContext(ctx, ` + SELECT v FROM cache WHERE k=$1 + `, k) + + var v []byte + if err := row.Scan(&v); err != nil { + return nil, err + } + + return v, row.Err() +} + +func (db Cache) Set(ctx context.Context, k string, v []byte) error { + if err := db.init(ctx); err != nil { + return err + } + + _, err := db.ExecContext(ctx, ` + INSERT INTO cache (k, v) VALUES ($1, $2) + ON CONFLICT DO UPDATE SET v=$2 WHERE k=$1 + `, k, v) + return err +} + +func (db Cache) init(ctx context.Context) error { + _, err := db.ExecContext(ctx, ` + CREATE TABLE IF NOT EXISTS cache(k TEXT PRIMARY KEY, v TEXT) + `) + return err +} diff --git a/cmd/db_test.go b/cmd/db_test.go new file mode 100644 index 0000000..b83db5e --- /dev/null +++ b/cmd/db_test.go @@ -0,0 +1,26 @@ +package cmd + +import ( + "bytes" + "context" + "path" + "testing" +) + +func TestCache(t *testing.T) { + ctx := context.Background() + cacheAddr = path.Join(t.TempDir(), "test.db") + + c := NewCache(ctx) + k := "k" + v := []byte("v") + if err := c.Set(ctx, k, v); err != nil { + t.Fatal(err) + } + + if got, err := c.Get(ctx, k); err != nil { + t.Fatal(err) + } else if !bytes.Equal(v, got) { + t.Fatalf("expected %q but got %q", v, got) + } +} diff --git a/cmd/main.go b/cmd/main.go deleted file mode 100644 index 32fda11..0000000 --- a/cmd/main.go +++ /dev/null @@ -1,75 +0,0 @@ -package cmd - -import ( - "context" - "encoding/json" - "fmt" - "log" - "os" - - "golang.org/x/time/rate" - "googlemaps.github.io/maps" -) - -func Run(ctx context.Context) error { - rps := 2 - limit := 200 - - limiter := rate.NewLimiter(rate.Limit(rps), 1) - - c, err := maps.NewClient(maps.WithAPIKey(os.Getenv("GOOGLE_PLACES_API_KEY"))) - if err != nil { - return err - } - - resp, err := c.TextSearch(ctx, &maps.TextSearchRequest{ - Query: os.Args[1], - Location: nil, - Radius: uint(0), - }) - if err != nil { - return err - } else if len(resp.Results) < 1 { - return fmt.Errorf("no results for %q", os.Args[1]) - } - origin := resp.Results[0].Geometry.Location - - type Result struct { - Name string - Lat float64 - Lng float64 - Address string - } - results := []Result{} - var nextToken string - for len(results) < limit { - limiter.Wait(ctx) - resp, err := c.TextSearch(ctx, &maps.TextSearchRequest{ - Query: os.Args[2], - Location: &origin, - Radius: uint(250), - PageToken: nextToken, - }) - if err != nil { - return err - } - for _, result := range resp.Results { - results = append(results, Result{ - Name: result.Name, - Lat: result.Geometry.Location.Lat, - Lng: result.Geometry.Location.Lng, - Address: result.FormattedAddress, - }) - } - nextToken = resp.NextPageToken - if nextToken == "" { - break - } else { - log.Printf("%d...", len(resp.Results)) - } - } - b, _ := json.Marshal(results) - fmt.Printf("%s\n", b) - - return ctx.Err() -} diff --git a/cmd/maps.go b/cmd/maps.go new file mode 100644 index 0000000..be185b8 --- /dev/null +++ b/cmd/maps.go @@ -0,0 +1,96 @@ +package cmd + +import ( + "context" + "fmt" + "os" + + "golang.org/x/time/rate" + "googlemaps.github.io/maps" +) + +type Maps struct { + around Location +} + +func NewMapsOf(ctx context.Context, town string) (*Maps, error) { + m := &Maps{} + var err error + m.around, err = m.textSearchOne(ctx, town) + return m, err +} + +var rps = 2 +var limit = 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.textSearch(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) textSearch(ctx context.Context, query string) ([]Location, error) { + locations := []Location{} + nextToken := "" + for { + 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 + } + } + 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(0) + if m.around == (Location{}) { + radius = 250 + } else { + 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 +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..5d627e0 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,9 @@ +package cmd + +import ( + "context" +) + +func Run(ctx context.Context) error { + return ctx.Err() +}