diff --git a/deleteme b/deleteme deleted file mode 100644 index c7cc753..0000000 --- a/deleteme +++ /dev/null @@ -1 +0,0 @@ -do eet \ No newline at end of file diff --git a/directions.go b/directions.go new file mode 100644 index 0000000..a1879bc --- /dev/null +++ b/directions.go @@ -0,0 +1,197 @@ +package maps + +import ( + "fmt" + "net/url" + "strings" + "time" +) + +const ( + AvoidTolls = "tolls" + AvoidHighways = "highways" + AvoidFerries = "ferries" + + ModeDriving = "driving" + ModeWalking = "walking" + ModeTransit = "transit" + ModeBicycling = "bicycling" + + UnitMetric = "metric" + UnitImperial = "imperial" + + StatusOK = "OK" + StatusNotFound = "NOT_FOUND" + StatusZeroResults = "ZERO_RESULTS" + StatusMaxWaypointsExceeded = "MAX_WAYPOINTS_EXCEEDED" + StatusInvalidRequest = "INVALID_REQUEST" + StatusRequestDenied = "REQUEST_DENIED" + StatusUnknownError = "UNKNOWN_ERROR" +) + +// TODO: via_waypoint not documented + +func (c Client) Directions(orig, dest Location, opts *DirectionsOpts) (*DirectionsResponse, error) { + var d DirectionsResponse + if err := c.do(baseURL+directions(orig, dest, opts), &d); err != nil { + return nil, err + } + return &d, nil +} + +func directions(orig, dest Location, opts *DirectionsOpts) string { + p := url.Values{} + p.Set("origin", orig.Location()) + p.Set("destination", dest.Location()) + opts.update(p) + return "directions/json?" + p.Encode() +} + +type DirectionsOpts struct { + Mode string + Waypoints Route + Alternatives bool + Avoid []string + Language string + Units string + Region string + DepartureTime time.Time + ArrivalTime time.Time +} + +func (do *DirectionsOpts) update(p url.Values) { + if do == nil { + return + } + if do.Mode != "" { + p.Set("mode", do.Mode) + } + if do.Waypoints != nil { + p.Set("waypoints", do.Waypoints.String()) + } + if do.Alternatives { + p.Set("alternatives", "true") + } + if do.Avoid != nil { + p.Set("avoid", strings.Join(do.Avoid, "|")) + } + if do.Language != "" { + p.Set("language", do.Language) + } + if do.Units != "" { + p.Set("units", do.Units) + } + if do.Region != "" { + p.Set("region", do.Region) + } + if !do.DepartureTime.IsZero() { + p.Set("departure_time", fmt.Sprintf("%d", do.DepartureTime.Unix())) + } + if !do.ArrivalTime.IsZero() { + p.Set("arrival_time", fmt.Sprintf("%d", do.ArrivalTime.Unix())) + } +} + +type DirectionsResponse struct { + Status string `json:"status"` + ErrorMessage string `json:"error_message"` + Routes []struct { + Summary string `json:"summary"` + Legs []struct { + Duration *Duration `json:"duration"` + DurationInTraffic *Duration `json:"duration_in_traffic"` + Distance *Distance `json:"distance"` + ArrivalTime Time `json:"arrival_time"` + DepartureTime Time `json:"departure_time"` + StartLocation *LatLng `json:"start_location"` + EndLocation *LatLng `json:"end_location"` + StartAddress string `json:"start_address"` + EndAddress string `json:"end_address"` + Steps []Step `json:"steps"` + } `json:"legs"` + Bounds Bounds `json:"bounds"` + Copyrights string `json:"copyrights"` + OverviewPolyline Polyline `json:"overview_polyline"` + Warnings []string `json:"warnings"` + WaypointOrder []int `json:"waypoint_order"` + } `json:"routes"` +} + +type Time struct { + Value int64 `json:"value"` + Text string `json:"text"` + TimeZone string `json:"time_zone"` +} + +func (t Time) Time() (*time.Time, error) { + l, err := time.LoadLocation(t.TimeZone) + if err != nil { + return nil, err + } + tm := time.Unix(t.Value, 0).In(l) + return &tm, nil +} + +type Step struct { + TravelMode string `json:"travel_mode"` // TODO: enum + StartLocation *LatLng `json:"start_location"` + EndLocation *LatLng `json:"end_location"` + Maneuver string `json:"maneuver"` // TODO: not documented? + Polyline *Polyline `json:"polyline"` + Duration *Duration `json:"duration"` + Distance *Distance `json:"distance"` + HTMLInstructions string `json:"html_instructions"` + Steps []Step `json:"steps"` // sub-steps + TransitDetails *struct { + ArrivalStop string `json:"arrival_stop"` + DepartureStop string `json:"departure_stop"` + ArrivalTime Time `json:"arrival_time"` + DepartureTime Time `json:"departure_time"` + Headsign string `json:"headsign"` + Headway int64 `json:"headway"` // Seconds until next departure, TODO: to time.Duration? + NumStops int `json:"num_stops"` + Line struct { + Name string `json:"name"` + ShortName string `json:"short_name"` + Color string `json:"color"` // hex color, TODO: to image.Color? + Agencies []struct { + Name string `json:"name"` + URL string `json:"url"` + Phone string `json:"phone"` + } `json:"agencies"` + URL string `json:"url"` + IconURL string `json:"icon"` + TextColor string `json:"text_color"` // hex color, TODO: to image.Color? + Vehicle *struct { + Name string `json:"name"` + Type string `json:"type"` // TODO: enum + IconURL string `json:"icon"` + } `json:"vehicle"` + } `json:"line"` + } `json:"transit_details"` +} + +type Bounds struct { + Northeast LatLng `json:"northeast"` + Southwest LatLng `json:"southwest"` +} + +type Duration struct { + Value int64 `json:"value"` + Text string `json:"text"` +} + +func (d Duration) Duration() time.Duration { + return time.Duration(d.Value) * time.Second +} + +type Distance struct { + Value int64 `json:"value"` // meters + Text string `json:"text"` +} + +type Polyline struct { + Points string `json:"points"` +} + +// TODO: methods to decode polyline points? diff --git a/maps.go b/maps.go new file mode 100644 index 0000000..d777f48 --- /dev/null +++ b/maps.go @@ -0,0 +1,87 @@ +package maps + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" +) + +const baseURL = "https://maps.googleapis.com/maps/api/" + +type Client struct { + Transport http.RoundTripper + Key string +} + +func NewClient(key string) Client { + return Client{Key: key, Transport: http.DefaultTransport} +} + +func (c Client) do(url string, r interface{}) error { + cl := &http.Client{Transport: c.Transport} + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return err + } + + if c.Key != "" { + q := req.URL.Query() + q.Set("key", c.Key) + req.URL.RawQuery = q.Encode() + } + + resp, err := cl.Get(url) + if err != nil { + return err + } + if resp.StatusCode != http.StatusOK { + return HTTPError{resp} + } + defer resp.Body.Close() + if err := json.NewDecoder(resp.Body).Decode(&r); err != nil { + return err + } + return nil +} + +type HTTPError struct { + Response *http.Response +} + +func (e HTTPError) Error() string { + return fmt.Sprintf("http error %d", e.Response.StatusCode) +} + +type Location interface { + Location() string +} + +type LatLng struct { + Lat float64 `json:"lat"` + Lng float64 `json:"lng"` +} + +func (ll LatLng) Location() string { + return fmt.Sprintf("%f,%f", ll.Lat, ll.Lng) +} + +func (ll LatLng) String() string { + return ll.Location() +} + +type Address string + +func (a Address) Location() string { + return string(a) +} + +type Route []Location + +func (r Route) String() string { + s := make([]string, len(r)) + for i, l := range r { + s[i] = l.Location() + } + return strings.Join(s, "|") +} diff --git a/maps_test.go b/maps_test.go new file mode 100644 index 0000000..ec84f22 --- /dev/null +++ b/maps_test.go @@ -0,0 +1,21 @@ +package maps + +import ( + "testing" + "time" +) + +func TestDirections(t *testing.T) { + c := NewClient("") + orig, dest := Address("111 8th Ave, NYC"), Address("170 E 92nd St, NYC") + opts := &DirectionsOpts{ + Mode: ModeWalking, + DepartureTime: time.Now(), + } + t.Logf("%s", baseURL+directions(orig, dest, opts)) + r, err := c.Directions(orig, dest, nil) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + t.Logf("%v", r) +}