2019-02-10 20:35:19 +01:00
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
2025-02-15 19:59:36 +01:00
|
|
|
"io"
|
|
|
|
|
"log/slog"
|
2019-02-12 17:01:02 +01:00
|
|
|
"net/http"
|
2019-03-02 11:43:58 +01:00
|
|
|
"strconv"
|
2019-02-26 14:54:16 +01:00
|
|
|
"strings"
|
2022-08-27 11:10:28 +02:00
|
|
|
|
2025-02-15 19:59:36 +01:00
|
|
|
"cloud.google.com/go/maps/routing/apiv2"
|
|
|
|
|
"cloud.google.com/go/maps/routing/apiv2/routingpb"
|
2022-08-27 11:10:28 +02:00
|
|
|
"github.com/caarlos0/env"
|
2025-02-15 19:59:36 +01:00
|
|
|
"google.golang.org/api/option"
|
|
|
|
|
"google.golang.org/genproto/googleapis/type/latlng"
|
|
|
|
|
"google.golang.org/grpc/metadata"
|
2022-08-27 11:10:28 +02:00
|
|
|
"googlemaps.github.io/maps"
|
2019-02-10 20:35:19 +01:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type config struct {
|
|
|
|
|
MapsApiKey string `env:"MAPS_API_KEY"`
|
2022-08-27 11:10:28 +02:00
|
|
|
Port int `env:"PORT" envDefault:"80"`
|
2019-02-10 20:35:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type location struct {
|
2022-08-27 11:10:28 +02:00
|
|
|
Lat float64 `json:"latitude"`
|
2019-02-26 14:54:16 +01:00
|
|
|
Long float64 `json:"longitude"`
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-02 11:43:58 +01:00
|
|
|
type address struct {
|
|
|
|
|
Address string `json:"address"`
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-26 14:54:16 +01:00
|
|
|
type distance struct {
|
2022-08-27 11:10:28 +02:00
|
|
|
Text string `json:"text"`
|
|
|
|
|
Value int `json:"value"`
|
2019-02-26 14:54:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type duration struct {
|
2022-08-27 11:10:28 +02:00
|
|
|
Text string `json:"text"`
|
2019-02-26 14:54:16 +01:00
|
|
|
Value float64 `json:"value"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type destination struct {
|
2022-08-27 11:10:28 +02:00
|
|
|
Destination string `json:"destination"`
|
|
|
|
|
Distance distance `json:"distance"`
|
|
|
|
|
Duration duration `json:"duration"`
|
2019-02-26 14:54:16 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type origin struct {
|
2022-08-27 11:10:28 +02:00
|
|
|
Origin string `json:"origin"`
|
2019-02-26 14:54:16 +01:00
|
|
|
Destinations []destination `json:"destinations"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type distanceResponse struct {
|
|
|
|
|
Origins []origin `json:"origins"`
|
2019-02-10 20:35:19 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
cfg := config{}
|
|
|
|
|
err := env.Parse(&cfg)
|
|
|
|
|
if err != nil {
|
|
|
|
|
fmt.Printf("%+v\n", err)
|
|
|
|
|
}
|
|
|
|
|
fmt.Printf("%+v\n", cfg)
|
|
|
|
|
|
2025-02-15 19:59:36 +01:00
|
|
|
mapsClient, err := maps.NewClient(maps.WithAPIKey(cfg.MapsApiKey))
|
2019-02-10 20:35:19 +01:00
|
|
|
if err != nil {
|
2025-02-15 19:59:36 +01:00
|
|
|
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
|
2019-02-10 20:35:19 +01:00
|
|
|
}
|
|
|
|
|
|
2025-02-15 19:59:36 +01:00
|
|
|
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")
|
|
|
|
|
}
|
2019-02-12 17:01:02 +01:00
|
|
|
}
|
2019-02-10 20:35:19 +01:00
|
|
|
|
2019-02-12 17:01:02 +01:00
|
|
|
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)
|
2019-02-10 20:35:19 +01:00
|
|
|
}
|
2019-02-12 17:01:02 +01:00
|
|
|
}
|
2019-02-26 14:54:16 +01:00
|
|
|
|
2025-02-15 19:59:36 +01:00
|
|
|
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},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
2022-08-27 11:10:28 +02:00
|
|
|
destinationsString := r.URL.Query().Get("destinations")
|
|
|
|
|
if destinationsString == "" {
|
|
|
|
|
x := make([]origin, len(origins))
|
|
|
|
|
for i, o := range origins {
|
2025-02-15 19:59:36 +01:00
|
|
|
x[i] = origin{Origin: o.Waypoint.GetAddress()}
|
2022-08-27 11:10:28 +02:00
|
|
|
}
|
|
|
|
|
writeResponse(distanceResponse{Origins: x}, w)
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-02-15 19:59:36 +01:00
|
|
|
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{
|
2022-08-27 11:10:28 +02:00
|
|
|
Origins: origins,
|
2019-02-26 14:54:16 +01:00
|
|
|
Destinations: destinations,
|
2025-02-15 19:59:36 +01:00
|
|
|
Units: routingpb.Units_METRIC,
|
2019-02-26 14:54:16 +01:00
|
|
|
}
|
2025-02-15 19:59:36 +01:00
|
|
|
response, err := client.ComputeRouteMatrix(ctx, matrixRequest)
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.With("error", err, "part", "handleDistanceMatrixRequest").Error("compute matrix")
|
2019-02-26 14:54:16 +01:00
|
|
|
w.WriteHeader(400)
|
2025-02-15 19:59:36 +01:00
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
res := distanceResponse{}
|
|
|
|
|
dests := make([][]destination, len(origins))
|
2025-02-16 16:23:41 +01:00
|
|
|
for i := range dests {
|
|
|
|
|
dests[i] = make([]destination, len(destinations))
|
|
|
|
|
}
|
2025-02-15 19:59:36 +01:00
|
|
|
for {
|
|
|
|
|
elem, err := response.Recv()
|
|
|
|
|
if err != nil {
|
|
|
|
|
if err == io.EOF {
|
|
|
|
|
break
|
2019-02-26 14:54:16 +01:00
|
|
|
}
|
2025-02-15 19:59:36 +01:00
|
|
|
slog.With("error", err, "part", "handleDistanceMatrixRequest").Error("fetch result")
|
|
|
|
|
w.WriteHeader(400)
|
|
|
|
|
return
|
2019-02-26 14:54:16 +01:00
|
|
|
}
|
2025-02-15 19:59:36 +01:00
|
|
|
ll := destinations[*elem.DestinationIndex].Waypoint.GetLocation().LatLng
|
|
|
|
|
|
2025-02-16 16:23:41 +01:00
|
|
|
dests[*elem.OriginIndex][*elem.DestinationIndex] = destination{
|
2025-02-15 19:59:36 +01:00
|
|
|
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(),
|
|
|
|
|
},
|
2025-02-16 16:23:41 +01:00
|
|
|
}
|
2025-02-15 19:59:36 +01:00
|
|
|
}
|
|
|
|
|
for i, dest := range dests {
|
|
|
|
|
res.Origins = append(res.Origins, origin{origins[i].Waypoint.GetAddress(), dest})
|
2022-08-27 11:10:28 +02:00
|
|
|
}
|
2025-02-15 19:59:36 +01:00
|
|
|
writeResponse(res, w)
|
2022-08-27 11:10:28 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeResponse(res distanceResponse, w http.ResponseWriter) {
|
|
|
|
|
if response, err := json.Marshal(res); err != nil {
|
2025-02-15 19:59:36 +01:00
|
|
|
slog.With("error", err, "part", "handleDistanceMatrixRequest").Error("marshal response error")
|
2022-08-27 11:10:28 +02:00
|
|
|
w.WriteHeader(404)
|
|
|
|
|
} else {
|
|
|
|
|
_, _ = w.Write(response)
|
2019-02-26 14:54:16 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-03-02 11:43:58 +01:00
|
|
|
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 {
|
2025-02-15 19:59:36 +01:00
|
|
|
slog.With("error", err, "part", "handleAddressRequest").Error("geocode")
|
2019-03-02 11:43:58 +01:00
|
|
|
w.WriteHeader(400)
|
|
|
|
|
} else {
|
|
|
|
|
if len(result) > 0 {
|
|
|
|
|
l := address{
|
2022-08-27 11:10:28 +02:00
|
|
|
Address: result[0].FormattedAddress,
|
2019-03-02 11:43:58 +01:00
|
|
|
}
|
|
|
|
|
if response, err := json.Marshal(l); err != nil {
|
2025-02-15 19:59:36 +01:00
|
|
|
slog.With("error", err, "part", "handleAddressRequest").Error("marshal response error")
|
2019-03-02 11:43:58 +01:00
|
|
|
w.WriteHeader(404)
|
|
|
|
|
} else {
|
|
|
|
|
_, _ = w.Write(response)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-12 17:01:02 +01:00
|
|
|
func handleLatLongRequest(w http.ResponseWriter, r *http.Request, client *maps.Client) {
|
|
|
|
|
req := &maps.GeocodingRequest{
|
2022-08-27 11:10:28 +02:00
|
|
|
Address: r.URL.Path[len("/latlong/"):],
|
2019-02-10 20:35:19 +01:00
|
|
|
}
|
2019-02-12 17:01:02 +01:00
|
|
|
if result, err := client.Geocode(context.Background(), req); err != nil {
|
|
|
|
|
w.WriteHeader(400)
|
|
|
|
|
} else {
|
|
|
|
|
if len(result) > 0 {
|
|
|
|
|
l := location{
|
2022-08-27 11:10:28 +02:00
|
|
|
Lat: result[0].Geometry.Location.Lat,
|
|
|
|
|
Long: result[0].Geometry.Location.Lng,
|
2019-02-12 17:01:02 +01:00
|
|
|
}
|
|
|
|
|
if response, err := json.Marshal(l); err != nil {
|
2020-01-17 14:53:15 +01:00
|
|
|
w.WriteHeader(500)
|
2019-02-12 17:01:02 +01:00
|
|
|
} else {
|
|
|
|
|
_, _ = w.Write(response)
|
|
|
|
|
}
|
2020-01-17 14:53:15 +01:00
|
|
|
} else {
|
2022-08-27 11:10:28 +02:00
|
|
|
w.WriteHeader(404)
|
|
|
|
|
}
|
2019-02-12 17:01:02 +01:00
|
|
|
}
|
2020-01-17 14:53:15 +01:00
|
|
|
}
|