Skip to content

Commit

Permalink
Geocoding + Distance + Duration without Google Maps (#19)
Browse files Browse the repository at this point in the history
A Google Maps API key is now optional.

This app will fallback to using OpenStreetMaps (OSM) for geocoding and Open Source Routing Machine (OSRM) for distance matrix calculation. Geocoding is done with OSM's nominatim service.
  • Loading branch information
kevinmichaelchen authored May 30, 2022
1 parent f7d6a0a commit 93a80a3
Show file tree
Hide file tree
Showing 19 changed files with 756 additions and 345 deletions.
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,15 @@ and [h3](https://h3geo.org/) (a hexagonal hierarchical geospatial indexing syste

## Project structure

| Directory | Description |
|-----------------------------------------------------|-------------------------------------------|
| [`./cmd`](./cmd) | CLI for making gRPC requests |
| [`./idl`](./idl/coop/drivers/dispatch/v1beta1) | Protobufs (Interface Definition Language) |
| [`./internal/app`](./internal/app) | App dependency injection / initialization |
| [`./internal/distance`](internal/service/distance) | Google Maps Distance Matrix logic |
| [`./internal/idl`](./internal/idl) | Auto-generated protobufs |
| [`./internal/models`](./internal/models) | Auto-generated ORM / models |
| [`./internal/service`](./internal/service) | Service layer / Business logic |
| [`./schema`](./schema) | SQL migration scripts |
| Directory | Description |
|--------------------------------------------------|-------------------------------------------|
| [`./cmd`](./cmd) | CLI for making gRPC requests |
| [`./idl`](./idl/coop/drivers/dispatch/v1beta1) | Protobufs (Interface Definition Language) |
| [`./internal/app`](./internal/app) | App dependency injection / initialization |
| [`./internal/idl`](./internal/idl) | Auto-generated protobufs |
| [`./internal/models`](./internal/models) | Auto-generated ORM / models |
| [`./internal/service`](./internal/service) | Service layer / Business logic |
| [`./schema`](./schema) | SQL migration scripts |

## How does it work

Expand Down
7 changes: 7 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,17 @@ go 1.18

require (
github.com/XSAM/otelsql v0.14.1
github.com/codingsince1985/geo-golang v1.8.1
github.com/envoyproxy/protoc-gen-validate v0.1.0
github.com/friendsofgo/errors v0.9.2
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
github.com/gojuno/go.osrm v0.1.0
github.com/google/go-cmp v0.5.8
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12
github.com/kr/pretty v0.2.0
github.com/lib/pq v1.10.5
github.com/paulmach/go.geo v0.0.0-20180829195134-22b514266d33
github.com/rs/xid v1.4.0
github.com/sethvargo/go-envconfig v0.6.0
github.com/spf13/cobra v1.4.0
Expand Down Expand Up @@ -39,6 +43,7 @@ require (
)

require (
github.com/benbjohnson/clock v1.3.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
Expand All @@ -52,9 +57,11 @@ require (
github.com/google/uuid v1.3.0 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/kr/text v0.1.0 // indirect
github.com/magiconair/properties v1.8.5 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/paulmach/go.geojson v1.4.0 // indirect
github.com/pelletier/go-toml v1.9.4 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_golang v1.12.1 // indirect
Expand Down
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/codingsince1985/geo-golang v1.8.1 h1:6B+Ce5QkbSglCtesiNRkSYMRDDQsYrv4XKM3jJVdyTw=
github.com/codingsince1985/geo-golang v1.8.1/go.mod h1:Ue7HAjKwwCAbqB5Q0YskqqnIX8XjMHL5Jq2fsSrI2T8=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
Expand Down Expand Up @@ -151,6 +153,8 @@ github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/gojuno/go.osrm v0.1.0 h1:M9RH1raBe7D72preWxFOw9WafGftGBQyoGNkG6upAT0=
github.com/gojuno/go.osrm v0.1.0/go.mod h1:XPCHB/Ir2/vHnqhKlfUxIiUGHFtTzgrRxD89JdkJhrs=
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
Expand Down Expand Up @@ -335,6 +339,11 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
github.com/paulmach/go.geo v0.0.0-20180829195134-22b514266d33 h1:doG/0aLlWE6E4ndyQlkAQrPwaojghwz1IlmH0kjTdyk=
github.com/paulmach/go.geo v0.0.0-20180829195134-22b514266d33/go.mod h1:btFYk/ltlMU7ZKguHS7zQrwHYCtLoXGTaa44OsPbEVw=
github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY=
github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs=
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
Expand Down
36 changes: 19 additions & 17 deletions internal/app/service/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,58 +3,60 @@ package service
import (
"database/sql"
"fmt"
"github.com/friendsofgo/errors"
"github.com/kevinmichaelchen/api-dispatch/internal/service"
"github.com/kevinmichaelchen/api-dispatch/internal/service/db"
"github.com/kevinmichaelchen/api-dispatch/internal/service/distance"
"github.com/kevinmichaelchen/api-dispatch/internal/service/geo"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.uber.org/fx"
"go.uber.org/zap"
"googlemaps.github.io/maps"
gmaps "googlemaps.github.io/maps"
"os"
)

var Module = fx.Module("service",
fx.Provide(
NewService,
NewDistanceService,
NewGeoService,
NewMapsClient,
NewDataStore,
),
)

type Params struct {
type ServiceParams struct {
fx.In
DataStore *db.Store
DistanceService *distance.Service `optional:"true"`
DistanceService *geo.Service
}

func NewService(p Params) *service.Service {
func NewService(p ServiceParams) *service.Service {
return service.NewService(p.DataStore, p.DistanceService)
}

func NewDataStore(sqlDB *sql.DB) *db.Store {
return db.NewStore(sqlDB)
}

func NewMapsClient() (*maps.Client, error) {
func NewMapsClient(logger *zap.Logger) (*gmaps.Client, error) {
apiKey := os.Getenv("API_KEY")
if apiKey == "" {
return nil, errors.New("missing API_KEY for Google Maps")
logger.Warn("Missing API Key for Google Maps... Initializing in degraded state...")
return nil, nil
}
c, err := maps.NewClient(
maps.WithAPIKey(apiKey),
maps.WithHTTPClient(otelhttp.DefaultClient),
c, err := gmaps.NewClient(
gmaps.WithAPIKey(apiKey),
gmaps.WithHTTPClient(otelhttp.DefaultClient),
)
if err != nil {
return nil, fmt.Errorf("failed to build Google Maps client: %w", err)
}
return c, nil
}

func NewDistanceService(logger *zap.Logger, client *maps.Client) (*distance.Service, error) {
if client == nil {
return nil, errors.New("no maps client")
}
return distance.NewService(client), nil
type GeoServiceParams struct {
fx.In
GoogleClient *gmaps.Client `optional:"true"`
}

func NewGeoService(logger *zap.Logger, p GeoServiceParams) (*geo.Service, error) {
return geo.NewService(p.GoogleClient, otelhttp.DefaultClient), nil
}
138 changes: 85 additions & 53 deletions internal/service/dispatch.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"context"
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
"github.com/kevinmichaelchen/api-dispatch/internal/idl/coop/drivers/dispatch/v1beta1"
"github.com/kevinmichaelchen/api-dispatch/internal/service/distance"
"github.com/kevinmichaelchen/api-dispatch/internal/service/money"
"github.com/kevinmichaelchen/api-dispatch/internal/service/ranking"
"github.com/kevinmichaelchen/api-dispatch/pkg/maps"
"github.com/kevinmichaelchen/api-dispatch/pkg/maps/distance"
"go.uber.org/zap"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/timestamppb"
Expand All @@ -23,15 +24,11 @@ func (s *Service) GetNearestDrivers(
ctx context.Context,
req *v1beta1.GetNearestDriversRequest,
) (*v1beta1.GetNearestDriversResponse, error) {
logger := ctxzap.Extract(ctx)

err := validate(req, req)
if err != nil {
return nil, err
}

trafficAware := s.distanceSvc != nil

// Query database
nearby, err := s.dataStore.GetNearbyDriverLocations(ctx, req.GetPickupLocation())
if err != nil {
Expand All @@ -55,32 +52,17 @@ func (s *Service) GetNearestDrivers(
results = results[:maxResults]
}

// In the event we have no Google Maps client and are operating in a
// degraded state, k-ring-sorting is still pretty good.
// The initial sort will be based on H3 resolutions and k-rings
results = ranking.SortResultsByKRing(results)

// Enrich results with distance/duration info from Google Maps API
// Enrich results (e.g., with distance/duration info, among other things)
var driverLocations []*v1beta1.LatLng
for _, result := range results {
driverLocations = append(driverLocations, result.GetLocation())
}
var pickupAddress string
if trafficAware {
out, err := s.distanceSvc.BetweenPoints(ctx, distance.BetweenPointsInput{
PickupLocations: []*v1beta1.LatLng{req.GetPickupLocation()},
DriverLocations: driverLocations,
})
if err != nil {
return nil, err
}
for i, info := range out.Info {
logger.Info("received distance matrix info", zap.Any("info", info))
results[i].Duration = durationpb.New(info.Duration)
results[i].DistanceMeters = float64(info.DistanceMeters)
// the driver is always the origin
results[i].Address = info.OriginAddress
pickupAddress = info.DestinationAddress
}
matrixOut, err := s.enrichNearbyDrivers(ctx, results, driverLocations, req.GetPickupLocation())
if err != nil {
return nil, err
}

// Final ranking/sorting pass
Expand All @@ -94,7 +76,7 @@ func (s *Service) GetNearestDrivers(

return &v1beta1.GetNearestDriversResponse{
Results: results,
PickupAddress: pickupAddress,
PickupAddress: matrixOut.DestinationAddresses[0],
}, nil
}

Expand All @@ -108,8 +90,6 @@ func (s *Service) GetNearestTrips(
return nil, err
}

trafficAware := s.distanceSvc != nil

// Query database
nearby, err := s.dataStore.GetNearbyTrips(ctx, req.GetDriverLocation())
if err != nil {
Expand All @@ -134,34 +114,19 @@ func (s *Service) GetNearestTrips(
results = results[:maxResults]
}

// In the event we have no Google Maps client and are operating in a
// degraded state, k-ring-sorting is still pretty good.
// The initial sort will be based on H3 resolutions and k-rings
results = ranking.SortResultsByKRing(results)

// Enrich results with distance/duration info from Google Maps API
var locations []*v1beta1.LatLng
// Enrich results (e.g., with distance/duration info, among other things)
var pickupLocations []*v1beta1.LatLng
for _, result := range results {
locations = append(locations, result.GetLocation())
pickupLocations = append(pickupLocations, result.GetLocation())
}
if trafficAware {
out, err := s.distanceSvc.BetweenPoints(ctx, distance.BetweenPointsInput{
PickupLocations: locations,
DriverLocations: []*v1beta1.LatLng{req.GetDriverLocation()},
})
if err != nil {
return nil, err
}
for i, info := range out.Info {
results[i].Duration = durationpb.New(info.Duration)
results[i].DistanceMeters = float64(info.DistanceMeters)
// the driver is always the origin, the pickup is the destination
results[i].Address = info.DestinationAddress
}
_, err = s.enrichNearbyTrips(ctx, results, req.GetDriverLocation(), pickupLocations)
if err != nil {
return nil, err
}

// Enrich results
enrichTripsWithFakeData(results)

// Final ranking/sorting pass
results = ranking.RankTrips(results)

Expand All @@ -176,13 +141,69 @@ func (s *Service) GetNearestTrips(
}, nil
}

func enrichTripsWithFakeData(in []*v1beta1.SearchResult) {
for idx := range in {
e := in[idx]
func (s *Service) enrichNearbyDrivers(
ctx context.Context,
results []*v1beta1.SearchResult,
driverLocations []*v1beta1.LatLng,
pickupLocation *v1beta1.LatLng,
) (*distance.MatrixResponse, error) {
logger := ctxzap.Extract(ctx)

out, err := s.distanceSvc.BetweenPoints(ctx, distance.BetweenPointsInput{
// the driver location(s) is/are always the origin(s)
Origins: toLatLngs(driverLocations),
Destinations: toLatLngs([]*v1beta1.LatLng{pickupLocation}),
})
if err != nil {
return nil, err
}

for i, row := range out.Rows {
for _, elem := range row.Elements {
logger.Info("Got Distance Matrix element", zap.Any("elem", elem))
results[i].Duration = durationpb.New(elem.Duration)
results[i].DistanceMeters = float64(elem.Distance)
results[i].Address = out.OriginAddresses[i]
}
}

return out, nil
}

func (s *Service) enrichNearbyTrips(
ctx context.Context,
results []*v1beta1.SearchResult,
driverLocation *v1beta1.LatLng,
pickupLocations []*v1beta1.LatLng,
) (*distance.MatrixResponse, error) {
logger := ctxzap.Extract(ctx)

out, err := s.distanceSvc.BetweenPoints(ctx, distance.BetweenPointsInput{
// the driver location(s) is/are always the origin(s)
Origins: toLatLngs([]*v1beta1.LatLng{driverLocation}),
Destinations: toLatLngs(pickupLocations),
})
if err != nil {
return nil, err
}

for idx := range results {
e := results[idx]
t := e.GetTrip()
t.ScheduledFor = timestamppb.New(randomTime())
t.ExpectedPayment = randomMoney()
}

for _, row := range out.Rows {
for i, elem := range row.Elements {
logger.Info("Got Distance Matrix element", zap.Any("elem", elem))
results[i].Duration = durationpb.New(elem.Duration)
results[i].DistanceMeters = float64(elem.Distance)
results[i].Address = out.DestinationAddresses[i]
}
}

return out, nil
}

func randomTime() time.Time {
Expand All @@ -197,3 +218,14 @@ func randomMoney() *v1beta1.Money {
f := float64(randomUnits) + (float64(randomCents) / float64(100))
return money.ConvertFloatToMoney(f)
}

func toLatLngs(in []*v1beta1.LatLng) []maps.LatLng {
var out []maps.LatLng
for _, e := range in {
out = append(out, maps.LatLng{
Lat: e.GetLatitude(),
Lng: e.GetLongitude(),
})
}
return out
}
Loading

0 comments on commit 93a80a3

Please sign in to comment.