Skip to content
This repository was archived by the owner on Jan 15, 2026. It is now read-only.

Commit 93a80a3

Browse files
Geocoding + Distance + Duration without Google Maps (#19)
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.
1 parent f7d6a0a commit 93a80a3

19 files changed

Lines changed: 756 additions & 345 deletions

File tree

README.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,15 @@ and [h3](https://h3geo.org/) (a hexagonal hierarchical geospatial indexing syste
2929

3030
## Project structure
3131

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

4342
## How does it work
4443

go.mod

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,17 @@ go 1.18
44

55
require (
66
github.com/XSAM/otelsql v0.14.1
7+
github.com/codingsince1985/geo-golang v1.8.1
78
github.com/envoyproxy/protoc-gen-validate v0.1.0
89
github.com/friendsofgo/errors v0.9.2
910
github.com/go-ozzo/ozzo-validation/v4 v4.3.0
11+
github.com/gojuno/go.osrm v0.1.0
1012
github.com/google/go-cmp v0.5.8
1113
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
1214
github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12
15+
github.com/kr/pretty v0.2.0
1316
github.com/lib/pq v1.10.5
17+
github.com/paulmach/go.geo v0.0.0-20180829195134-22b514266d33
1418
github.com/rs/xid v1.4.0
1519
github.com/sethvargo/go-envconfig v0.6.0
1620
github.com/spf13/cobra v1.4.0
@@ -39,6 +43,7 @@ require (
3943
)
4044

4145
require (
46+
github.com/benbjohnson/clock v1.3.0 // indirect
4247
github.com/beorn7/perks v1.0.1 // indirect
4348
github.com/cespare/xxhash/v2 v2.1.2 // indirect
4449
github.com/davecgh/go-spew v1.1.1 // indirect
@@ -52,9 +57,11 @@ require (
5257
github.com/google/uuid v1.3.0 // indirect
5358
github.com/hashicorp/hcl v1.0.0 // indirect
5459
github.com/inconshreveable/mousetrap v1.0.0 // indirect
60+
github.com/kr/text v0.1.0 // indirect
5561
github.com/magiconair/properties v1.8.5 // indirect
5662
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
5763
github.com/mitchellh/mapstructure v1.4.2 // indirect
64+
github.com/paulmach/go.geojson v1.4.0 // indirect
5865
github.com/pelletier/go-toml v1.9.4 // indirect
5966
github.com/pmezard/go-difflib v1.0.0 // indirect
6067
github.com/prometheus/client_golang v1.12.1 // indirect

go.sum

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH
9595
github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
9696
github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
9797
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
98+
github.com/codingsince1985/geo-golang v1.8.1 h1:6B+Ce5QkbSglCtesiNRkSYMRDDQsYrv4XKM3jJVdyTw=
99+
github.com/codingsince1985/geo-golang v1.8.1/go.mod h1:Ue7HAjKwwCAbqB5Q0YskqqnIX8XjMHL5Jq2fsSrI2T8=
98100
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
99101
github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
100102
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
@@ -151,6 +153,8 @@ github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRx
151153
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
152154
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
153155
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
156+
github.com/gojuno/go.osrm v0.1.0 h1:M9RH1raBe7D72preWxFOw9WafGftGBQyoGNkG6upAT0=
157+
github.com/gojuno/go.osrm v0.1.0/go.mod h1:XPCHB/Ir2/vHnqhKlfUxIiUGHFtTzgrRxD89JdkJhrs=
154158
github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
155159
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
156160
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
@@ -335,6 +339,11 @@ github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRW
335339
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
336340
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
337341
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
342+
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
343+
github.com/paulmach/go.geo v0.0.0-20180829195134-22b514266d33 h1:doG/0aLlWE6E4ndyQlkAQrPwaojghwz1IlmH0kjTdyk=
344+
github.com/paulmach/go.geo v0.0.0-20180829195134-22b514266d33/go.mod h1:btFYk/ltlMU7ZKguHS7zQrwHYCtLoXGTaa44OsPbEVw=
345+
github.com/paulmach/go.geojson v1.4.0 h1:5x5moCkCtDo5x8af62P9IOAYGQcYHtxz2QJ3x1DoCgY=
346+
github.com/paulmach/go.geojson v1.4.0/go.mod h1:YaKx1hKpWF+T2oj2lFJPsW/t1Q5e1jQI61eoQSTwpIs=
338347
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
339348
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
340349
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=

internal/app/service/service.go

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,58 +3,60 @@ package service
33
import (
44
"database/sql"
55
"fmt"
6-
"github.com/friendsofgo/errors"
76
"github.com/kevinmichaelchen/api-dispatch/internal/service"
87
"github.com/kevinmichaelchen/api-dispatch/internal/service/db"
9-
"github.com/kevinmichaelchen/api-dispatch/internal/service/distance"
8+
"github.com/kevinmichaelchen/api-dispatch/internal/service/geo"
109
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
1110
"go.uber.org/fx"
1211
"go.uber.org/zap"
13-
"googlemaps.github.io/maps"
12+
gmaps "googlemaps.github.io/maps"
1413
"os"
1514
)
1615

1716
var Module = fx.Module("service",
1817
fx.Provide(
1918
NewService,
20-
NewDistanceService,
19+
NewGeoService,
2120
NewMapsClient,
2221
NewDataStore,
2322
),
2423
)
2524

26-
type Params struct {
25+
type ServiceParams struct {
2726
fx.In
2827
DataStore *db.Store
29-
DistanceService *distance.Service `optional:"true"`
28+
DistanceService *geo.Service
3029
}
3130

32-
func NewService(p Params) *service.Service {
31+
func NewService(p ServiceParams) *service.Service {
3332
return service.NewService(p.DataStore, p.DistanceService)
3433
}
3534

3635
func NewDataStore(sqlDB *sql.DB) *db.Store {
3736
return db.NewStore(sqlDB)
3837
}
3938

40-
func NewMapsClient() (*maps.Client, error) {
39+
func NewMapsClient(logger *zap.Logger) (*gmaps.Client, error) {
4140
apiKey := os.Getenv("API_KEY")
4241
if apiKey == "" {
43-
return nil, errors.New("missing API_KEY for Google Maps")
42+
logger.Warn("Missing API Key for Google Maps... Initializing in degraded state...")
43+
return nil, nil
4444
}
45-
c, err := maps.NewClient(
46-
maps.WithAPIKey(apiKey),
47-
maps.WithHTTPClient(otelhttp.DefaultClient),
45+
c, err := gmaps.NewClient(
46+
gmaps.WithAPIKey(apiKey),
47+
gmaps.WithHTTPClient(otelhttp.DefaultClient),
4848
)
4949
if err != nil {
5050
return nil, fmt.Errorf("failed to build Google Maps client: %w", err)
5151
}
5252
return c, nil
5353
}
5454

55-
func NewDistanceService(logger *zap.Logger, client *maps.Client) (*distance.Service, error) {
56-
if client == nil {
57-
return nil, errors.New("no maps client")
58-
}
59-
return distance.NewService(client), nil
55+
type GeoServiceParams struct {
56+
fx.In
57+
GoogleClient *gmaps.Client `optional:"true"`
58+
}
59+
60+
func NewGeoService(logger *zap.Logger, p GeoServiceParams) (*geo.Service, error) {
61+
return geo.NewService(p.GoogleClient, otelhttp.DefaultClient), nil
6062
}

internal/service/dispatch.go

Lines changed: 85 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@ import (
44
"context"
55
"github.com/grpc-ecosystem/go-grpc-middleware/logging/zap/ctxzap"
66
"github.com/kevinmichaelchen/api-dispatch/internal/idl/coop/drivers/dispatch/v1beta1"
7-
"github.com/kevinmichaelchen/api-dispatch/internal/service/distance"
87
"github.com/kevinmichaelchen/api-dispatch/internal/service/money"
98
"github.com/kevinmichaelchen/api-dispatch/internal/service/ranking"
9+
"github.com/kevinmichaelchen/api-dispatch/pkg/maps"
10+
"github.com/kevinmichaelchen/api-dispatch/pkg/maps/distance"
1011
"go.uber.org/zap"
1112
"google.golang.org/protobuf/types/known/durationpb"
1213
"google.golang.org/protobuf/types/known/timestamppb"
@@ -23,15 +24,11 @@ func (s *Service) GetNearestDrivers(
2324
ctx context.Context,
2425
req *v1beta1.GetNearestDriversRequest,
2526
) (*v1beta1.GetNearestDriversResponse, error) {
26-
logger := ctxzap.Extract(ctx)
27-
2827
err := validate(req, req)
2928
if err != nil {
3029
return nil, err
3130
}
3231

33-
trafficAware := s.distanceSvc != nil
34-
3532
// Query database
3633
nearby, err := s.dataStore.GetNearbyDriverLocations(ctx, req.GetPickupLocation())
3734
if err != nil {
@@ -55,32 +52,17 @@ func (s *Service) GetNearestDrivers(
5552
results = results[:maxResults]
5653
}
5754

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

62-
// Enrich results with distance/duration info from Google Maps API
58+
// Enrich results (e.g., with distance/duration info, among other things)
6359
var driverLocations []*v1beta1.LatLng
6460
for _, result := range results {
6561
driverLocations = append(driverLocations, result.GetLocation())
6662
}
67-
var pickupAddress string
68-
if trafficAware {
69-
out, err := s.distanceSvc.BetweenPoints(ctx, distance.BetweenPointsInput{
70-
PickupLocations: []*v1beta1.LatLng{req.GetPickupLocation()},
71-
DriverLocations: driverLocations,
72-
})
73-
if err != nil {
74-
return nil, err
75-
}
76-
for i, info := range out.Info {
77-
logger.Info("received distance matrix info", zap.Any("info", info))
78-
results[i].Duration = durationpb.New(info.Duration)
79-
results[i].DistanceMeters = float64(info.DistanceMeters)
80-
// the driver is always the origin
81-
results[i].Address = info.OriginAddress
82-
pickupAddress = info.DestinationAddress
83-
}
63+
matrixOut, err := s.enrichNearbyDrivers(ctx, results, driverLocations, req.GetPickupLocation())
64+
if err != nil {
65+
return nil, err
8466
}
8567

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

9577
return &v1beta1.GetNearestDriversResponse{
9678
Results: results,
97-
PickupAddress: pickupAddress,
79+
PickupAddress: matrixOut.DestinationAddresses[0],
9880
}, nil
9981
}
10082

@@ -108,8 +90,6 @@ func (s *Service) GetNearestTrips(
10890
return nil, err
10991
}
11092

111-
trafficAware := s.distanceSvc != nil
112-
11393
// Query database
11494
nearby, err := s.dataStore.GetNearbyTrips(ctx, req.GetDriverLocation())
11595
if err != nil {
@@ -134,34 +114,19 @@ func (s *Service) GetNearestTrips(
134114
results = results[:maxResults]
135115
}
136116

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

141-
// Enrich results with distance/duration info from Google Maps API
142-
var locations []*v1beta1.LatLng
120+
// Enrich results (e.g., with distance/duration info, among other things)
121+
var pickupLocations []*v1beta1.LatLng
143122
for _, result := range results {
144-
locations = append(locations, result.GetLocation())
123+
pickupLocations = append(pickupLocations, result.GetLocation())
145124
}
146-
if trafficAware {
147-
out, err := s.distanceSvc.BetweenPoints(ctx, distance.BetweenPointsInput{
148-
PickupLocations: locations,
149-
DriverLocations: []*v1beta1.LatLng{req.GetDriverLocation()},
150-
})
151-
if err != nil {
152-
return nil, err
153-
}
154-
for i, info := range out.Info {
155-
results[i].Duration = durationpb.New(info.Duration)
156-
results[i].DistanceMeters = float64(info.DistanceMeters)
157-
// the driver is always the origin, the pickup is the destination
158-
results[i].Address = info.DestinationAddress
159-
}
125+
_, err = s.enrichNearbyTrips(ctx, results, req.GetDriverLocation(), pickupLocations)
126+
if err != nil {
127+
return nil, err
160128
}
161129

162-
// Enrich results
163-
enrichTripsWithFakeData(results)
164-
165130
// Final ranking/sorting pass
166131
results = ranking.RankTrips(results)
167132

@@ -176,13 +141,69 @@ func (s *Service) GetNearestTrips(
176141
}, nil
177142
}
178143

179-
func enrichTripsWithFakeData(in []*v1beta1.SearchResult) {
180-
for idx := range in {
181-
e := in[idx]
144+
func (s *Service) enrichNearbyDrivers(
145+
ctx context.Context,
146+
results []*v1beta1.SearchResult,
147+
driverLocations []*v1beta1.LatLng,
148+
pickupLocation *v1beta1.LatLng,
149+
) (*distance.MatrixResponse, error) {
150+
logger := ctxzap.Extract(ctx)
151+
152+
out, err := s.distanceSvc.BetweenPoints(ctx, distance.BetweenPointsInput{
153+
// the driver location(s) is/are always the origin(s)
154+
Origins: toLatLngs(driverLocations),
155+
Destinations: toLatLngs([]*v1beta1.LatLng{pickupLocation}),
156+
})
157+
if err != nil {
158+
return nil, err
159+
}
160+
161+
for i, row := range out.Rows {
162+
for _, elem := range row.Elements {
163+
logger.Info("Got Distance Matrix element", zap.Any("elem", elem))
164+
results[i].Duration = durationpb.New(elem.Duration)
165+
results[i].DistanceMeters = float64(elem.Distance)
166+
results[i].Address = out.OriginAddresses[i]
167+
}
168+
}
169+
170+
return out, nil
171+
}
172+
173+
func (s *Service) enrichNearbyTrips(
174+
ctx context.Context,
175+
results []*v1beta1.SearchResult,
176+
driverLocation *v1beta1.LatLng,
177+
pickupLocations []*v1beta1.LatLng,
178+
) (*distance.MatrixResponse, error) {
179+
logger := ctxzap.Extract(ctx)
180+
181+
out, err := s.distanceSvc.BetweenPoints(ctx, distance.BetweenPointsInput{
182+
// the driver location(s) is/are always the origin(s)
183+
Origins: toLatLngs([]*v1beta1.LatLng{driverLocation}),
184+
Destinations: toLatLngs(pickupLocations),
185+
})
186+
if err != nil {
187+
return nil, err
188+
}
189+
190+
for idx := range results {
191+
e := results[idx]
182192
t := e.GetTrip()
183193
t.ScheduledFor = timestamppb.New(randomTime())
184194
t.ExpectedPayment = randomMoney()
185195
}
196+
197+
for _, row := range out.Rows {
198+
for i, elem := range row.Elements {
199+
logger.Info("Got Distance Matrix element", zap.Any("elem", elem))
200+
results[i].Duration = durationpb.New(elem.Duration)
201+
results[i].DistanceMeters = float64(elem.Distance)
202+
results[i].Address = out.DestinationAddresses[i]
203+
}
204+
}
205+
206+
return out, nil
186207
}
187208

188209
func randomTime() time.Time {
@@ -197,3 +218,14 @@ func randomMoney() *v1beta1.Money {
197218
f := float64(randomUnits) + (float64(randomCents) / float64(100))
198219
return money.ConvertFloatToMoney(f)
199220
}
221+
222+
func toLatLngs(in []*v1beta1.LatLng) []maps.LatLng {
223+
var out []maps.LatLng
224+
for _, e := range in {
225+
out = append(out, maps.LatLng{
226+
Lat: e.GetLatitude(),
227+
Lng: e.GetLongitude(),
228+
})
229+
}
230+
return out
231+
}

0 commit comments

Comments
 (0)