Files
argoyle b3e9b723a4 fix: correct destination initialization and response handling
This commit makes corrections to the initialization of the dests array to 
ensure it properly allocates space for each destination. It also fixes the 
response handling by correctly extending the response structure after 
the loop, improving overall stability and accuracy of the distance 
matrix request handling.
2025-02-16 16:23:41 +01:00

243 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 i := range dests {
dests[i] = make([]destination, len(destinations))
}
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][*elem.DestinationIndex] = 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)
}
}
}