Files
geo-service/main.go
T
argoyle 53c89cb7e4 refactor: update dependencies and improve distance matrix handling
Updates Go module dependencies to the latest compatible versions, 
including the integration of Cloud Maps routing API. Refactors the 
distance matrix handling to utilize the new routing client, 
ensuring proper request formation and error handling for lat/lng 
parsing. This enhances functionality and maintains compatibility 
with updated libraries.
2025-02-15 19:59:36 +01:00

241 lines
6.7 KiB
Go

package main
import (
"context"
"encoding/json"
"fmt"
"io"
"log/slog"
"net/http"
"strconv"
"strings"
"cloud.google.com/go/maps/routing/apiv2"
"cloud.google.com/go/maps/routing/apiv2/routingpb"
"github.com/caarlos0/env"
"google.golang.org/api/option"
"google.golang.org/genproto/googleapis/type/latlng"
"google.golang.org/grpc/metadata"
"googlemaps.github.io/maps"
)
type config struct {
MapsApiKey string `env:"MAPS_API_KEY"`
Port int `env:"PORT" envDefault:"80"`
}
type location struct {
Lat float64 `json:"latitude"`
Long float64 `json:"longitude"`
}
type address struct {
Address string `json:"address"`
}
type distance struct {
Text string `json:"text"`
Value int `json:"value"`
}
type duration struct {
Text string `json:"text"`
Value float64 `json:"value"`
}
type destination struct {
Destination string `json:"destination"`
Distance distance `json:"distance"`
Duration duration `json:"duration"`
}
type origin struct {
Origin string `json:"origin"`
Destinations []destination `json:"destinations"`
}
type distanceResponse struct {
Origins []origin `json:"origins"`
}
func main() {
cfg := config{}
err := env.Parse(&cfg)
if err != nil {
fmt.Printf("%+v\n", err)
}
fmt.Printf("%+v\n", cfg)
mapsClient, err := maps.NewClient(maps.WithAPIKey(cfg.MapsApiKey))
if err != nil {
slog.With("error", err).Error("new maps client")
return
}
routesClient, err := routing.NewRoutesClient(context.Background(), option.WithAPIKey(cfg.MapsApiKey))
if err != nil {
slog.With("error", err).Error("new routes client")
return
}
http.HandleFunc("/latlong/", makeHandler(handleLatLongRequest, mapsClient))
http.HandleFunc("/address/", makeHandler(handleAddressRequest, mapsClient))
http.HandleFunc("/distance/", func(w http.ResponseWriter, r *http.Request) {
handleDistanceMatrixRequest(w, r, routesClient)
})
if err := http.ListenAndServe(fmt.Sprintf(":%d", cfg.Port), nil); err != nil {
slog.With("error", err).Error("listen and serve")
}
}
func makeHandler(fn func(http.ResponseWriter, *http.Request, *maps.Client), client *maps.Client) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fn(w, r, client)
}
}
func handleDistanceMatrixRequest(w http.ResponseWriter, r *http.Request, client *routing.RoutesClient) {
ctx := context.Background()
ctx = metadata.AppendToOutgoingContext(ctx, "X-Goog-Fieldmask", "originIndex,destinationIndex,duration,distanceMeters,status")
origins := make([]*routingpb.RouteMatrixOrigin, 0)
destinations := make([]*routingpb.RouteMatrixDestination, 0)
for _, origin := range strings.Split(r.URL.Query().Get("origins"), "|") {
origins = append(origins, &routingpb.RouteMatrixOrigin{
Waypoint: &routingpb.Waypoint{
LocationType: &routingpb.Waypoint_Address{Address: origin},
},
})
}
destinationsString := r.URL.Query().Get("destinations")
if destinationsString == "" {
x := make([]origin, len(origins))
for i, o := range origins {
x[i] = origin{Origin: o.Waypoint.GetAddress()}
}
writeResponse(distanceResponse{Origins: x}, w)
return
}
for _, destination := range strings.Split(destinationsString, "|") {
parts := strings.Split(destination, ",")
lat, err := strconv.ParseFloat(parts[0], 64)
if err != nil {
slog.With("error", err, "part", "handleDistanceMatrixRequest", "latlng", destination).Error("unable to parse latitude")
w.WriteHeader(400)
return
}
lng, err := strconv.ParseFloat(parts[1], 64)
if err != nil {
slog.With("error", err, "part", "handleDistanceMatrixRequest", "latlng", destination).Error("unable to parse longitude")
w.WriteHeader(400)
return
}
destinations = append(destinations, &routingpb.RouteMatrixDestination{
Waypoint: &routingpb.Waypoint{
LocationType: &routingpb.Waypoint_Location{Location: &routingpb.Location{LatLng: &latlng.LatLng{
Latitude: lat,
Longitude: lng,
}}},
},
})
}
matrixRequest := &routingpb.ComputeRouteMatrixRequest{
Origins: origins,
Destinations: destinations,
Units: routingpb.Units_METRIC,
}
response, err := client.ComputeRouteMatrix(ctx, matrixRequest)
if err != nil {
slog.With("error", err, "part", "handleDistanceMatrixRequest").Error("compute matrix")
w.WriteHeader(400)
return
}
res := distanceResponse{}
dests := make([][]destination, len(origins))
for {
elem, err := response.Recv()
if err != nil {
if err == io.EOF {
break
}
slog.With("error", err, "part", "handleDistanceMatrixRequest").Error("fetch result")
w.WriteHeader(400)
return
}
ll := destinations[*elem.DestinationIndex].Waypoint.GetLocation().LatLng
dests[*elem.OriginIndex] = append(dests[*elem.OriginIndex], destination{
Destination: fmt.Sprintf("%f,%f", ll.Latitude, ll.Longitude),
Distance: distance{
Text: string(elem.DistanceMeters),
Value: int(elem.DistanceMeters),
},
Duration: duration{
elem.Duration.AsDuration().String(),
elem.Duration.AsDuration().Minutes(),
},
})
}
for i, dest := range dests {
res.Origins = append(res.Origins, origin{origins[i].Waypoint.GetAddress(), dest})
}
writeResponse(res, w)
}
func writeResponse(res distanceResponse, w http.ResponseWriter) {
if response, err := json.Marshal(res); err != nil {
slog.With("error", err, "part", "handleDistanceMatrixRequest").Error("marshal response error")
w.WriteHeader(404)
} else {
_, _ = w.Write(response)
}
}
func handleAddressRequest(w http.ResponseWriter, r *http.Request, client *maps.Client) {
p := strings.Split(r.URL.Path[len("/address/"):], ",")
lat, _ := strconv.ParseFloat(p[0], 64)
lng, _ := strconv.ParseFloat(p[1], 64)
req := &maps.GeocodingRequest{
LatLng: &maps.LatLng{Lat: lat, Lng: lng},
}
if result, err := client.Geocode(context.Background(), req); err != nil {
slog.With("error", err, "part", "handleAddressRequest").Error("geocode")
w.WriteHeader(400)
} else {
if len(result) > 0 {
l := address{
Address: result[0].FormattedAddress,
}
if response, err := json.Marshal(l); err != nil {
slog.With("error", err, "part", "handleAddressRequest").Error("marshal response error")
w.WriteHeader(404)
} else {
_, _ = w.Write(response)
}
}
}
}
func handleLatLongRequest(w http.ResponseWriter, r *http.Request, client *maps.Client) {
req := &maps.GeocodingRequest{
Address: r.URL.Path[len("/latlong/"):],
}
if result, err := client.Geocode(context.Background(), req); err != nil {
w.WriteHeader(400)
} else {
if len(result) > 0 {
l := location{
Lat: result[0].Geometry.Location.Lat,
Long: result[0].Geometry.Location.Lng,
}
if response, err := json.Marshal(l); err != nil {
w.WriteHeader(500)
} else {
_, _ = w.Write(response)
}
} else {
w.WriteHeader(404)
}
}
}