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) } } }