Skip to content

Instantly share code, notes, and snippets.

@imjasonh
Last active October 28, 2021 06:20
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save imjasonh/cbfa5a05023549bcf565 to your computer and use it in GitHub Desktop.
Save imjasonh/cbfa5a05023549bcf565 to your computer and use it in GitHub Desktop.
Maps API client in Go
package maps
import (
"math/rand"
"net/http"
"time"
)
type backoff struct {
Transport http.RoundTripper
MaxTries int
tries int
sleep func(time.Duration)
}
func (bt *backoff) RoundTrip(req *http.Request) (*http.Response, error) {
if bt.MaxTries == 0 {
bt.MaxTries = 5
}
if bt.sleep == nil {
bt.sleep = time.Sleep
}
wait := time.Second
var err error
for ; bt.tries < bt.MaxTries; bt.tries++ {
var resp *http.Response
resp, err = bt.Transport.RoundTrip(req)
if err != nil {
// fallthrough, retry
} else if resp.StatusCode >= http.StatusInternalServerError {
err = HTTPError{resp}
} else {
return resp, nil
}
if bt.tries == bt.MaxTries-1 {
// last try failed, just give up
continue
}
bt.sleep(wait)
jitter := time.Duration(rand.Intn(1000))*time.Millisecond - 500*time.Millisecond // jitter is +/- .5s
wait = wait*2 + jitter
}
return nil, err
}
package maps
import (
"net/http"
"net/http/httptest"
"testing"
"time"
)
func TestBackoff(t *testing.T) {
s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "fail", http.StatusInternalServerError)
}))
defer s.Close()
tries := 10
sleeps := 0
bt := &backoff{
MaxTries: tries,
Transport: http.DefaultTransport,
sleep: func(d time.Duration) {
sleeps += 1
t.Logf("sleeping %s", d)
},
}
c := &http.Client{Transport: bt}
r, _ := http.NewRequest("GET", s.URL, nil)
if _, err := c.Do(r); err == nil {
t.Errorf("expected error")
}
if sleeps != tries-1 {
t.Errorf("unexpected # of calls to sleep, got %d, want %d", tries-1, sleeps)
}
if bt.tries != tries {
t.Errorf("got %d tries, want %d", bt.tries, tries)
}
}
package maps
import (
"net/http"
"golang.org/x/net/context"
)
type contextKey int
// NewContext returns a new context that uses the provided http.Client and API key.
func NewContext(key string, c *http.Client) context.Context {
return WithContext(context.Background(), key, c)
}
// WithContext returns a new context in a similar way NewContext does, but initiates the new context with the specified parent.
func WithContext(parent context.Context, key string, c *http.Client) context.Context {
vals := map[string]interface{}{}
vals["key"] = key
vals["httpClient"] = c
return context.WithValue(parent, contextKey(0), vals)
}
// NewWorkContext returns a new context that uses the provided http.Client and Google Maps API for Work client ID and private key.
func NewWorkContext(clientID, privateKey string, c *http.Client) context.Context {
return WithWorkCredentials(context.Background(), clientID, privateKey, c)
}
// WithWorkCredentials returns a new context in a similar way NewWorkContext does, but initiates the new context with the specified parent.
func WithWorkCredentials(parent context.Context, clientID, privateKey string, c *http.Client) context.Context {
vals := map[string]interface{}{}
vals["workClientID"] = clientID
vals["workPrivKey"] = privateKey
vals["httpClient"] = c
return context.WithValue(parent, contextKey(0), vals)
}
func key(ctx context.Context) string {
k, found := ctx.Value(contextKey(0)).(map[string]interface{})["key"]
if found {
return k.(string)
}
return ""
}
func workCreds(ctx context.Context) (string, string) {
var clientID string
cid, found := ctx.Value(contextKey(0)).(map[string]interface{})["workClientID"]
if found {
clientID = cid.(string)
}
var privateKey string
pkey, found := ctx.Value(contextKey(0)).(map[string]interface{})["workPrivKey"]
if found {
privateKey = pkey.(string)
}
return clientID, privateKey
}
func httpClient(ctx context.Context) *http.Client {
cl, found := ctx.Value(contextKey(0)).(map[string]interface{})["httpClient"]
if found && cl != nil {
return cl.(*http.Client)
}
return &http.Client{Transport: &backoff{Transport: http.DefaultTransport}}
}
package maps
import (
"fmt"
"net/url"
"strings"
"time"
"golang.org/x/net/context"
)
const (
// AvoidTolls indicates that the route should avoid toll roads.
AvoidTolls = "tolls"
// AvoidHighways indicates that the route should avoid highways.
AvoidHighways = "highways"
// AvoidFerries indicates that the route should avoid ferries.
AvoidFerries = "ferries"
// ModeDriving indicates that driving directions are requested.
ModeDriving = "driving"
// ModeWalking indicates that walking directions are requested.
ModeWalking = "walking"
// ModeTransit indicates that transit directions are requested.
ModeTransit = "transit"
// ModeBicycling indicates that bicycling directions are requested.
ModeBicycling = "bicycling"
// UnitMetric indicates that the results should be stated in metric units.
UnitMetric = "metric"
// UnitImperial indicates that the results should be stated in imperial units.
UnitImperial = "imperial"
)
// TODO: via_waypoint not documented
// Directions requests routes between orig and dest Locations.
//
// See https://developers.google.com/maps/documentation/directions/
func Directions(ctx context.Context, orig, dest Location, opts *DirectionsOpts) ([]Route, error) {
var d directionsResponse
if err := doDecode(ctx, baseURL+directions(orig, dest, opts), &d); err != nil {
return nil, err
}
if d.Status != StatusOK {
return nil, APIError{d.Status, d.ErrorMessage}
}
return d.Routes, 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()
}
// DirectionsOpts defines options for Directions requests.
type DirectionsOpts struct {
// Waypoints alter a route by routing it through the specified location(s).
//
// Waypoints are only supported for driving, walking and bicycling directions.
//
// See https://developers.google.com/maps/documentation/directions/#Waypoints
Waypoints []Location
// If true and if waypoints are specified, the Directions API may optimize the provided route by rearranging the waypoints in a more efficient order.
OptimizeWaypoints bool
// If true, multiple Routes may be returned. This may increase the response time from the server.
Alternatives bool
// Avoid indicates that the calculated route(s) should avoid the indicated features.
//
// Accepted values are AvoidTolls, AvoidHighways and AvoidFerries
//
// See https://developers.google.com/maps/documentation/directions/#Restrictions
Avoid []string
// Specifies the mode of transport to use when calculating directions.
//
// Accepted values are ModeDriving (the default), ModeWalking, ModeTransit and ModeBicycling.
// If ModeTransit is specified, either DepartureTime or ArrivalTime must also be specified.
//
// See https://developers.google.com/maps/documentation/directions/#TravelModes
Mode string
// The language in which to return results.
//
// See https://developers.google.com/maps/faq#languagesupport
Language string
// Specifies the unit system to use when displaying results.
//
// Accepted values are UnitMetric and UnitImperial.
//
// See https://developers.google.com/maps/documentation/directions/#UnitSystems
Units string
// The region code, specified as a ccTLD ("top-level domain") two-character value.
//
// See https://developers.google.com/maps/documentation/directions/#RegionBiasing
Region string
// Specifies the desired time of departure.
//
// The departure time can be specified for transit directions, or for Google Maps API for Work
// clients to receive trip duration considering current traffic conditions.
DepartureTime time.Time
// Specifies the desired time of arrival for transit directions.
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 {
v := ""
if do.OptimizeWaypoints {
v = "optimize:true|"
}
p.Set("waypoints", v+encodeLocations(do.Waypoints))
}
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 []Route `json:"routes"`
}
// Route describes a possible route between the requested origin and destination.
type Route struct {
// Summary contains a short textual description of the route, suitable for naming and disambiguating the route from alternatives.
Summary string `json:"summary"`
// Legs contains information about the legs of a route, between two locations within the route.
// A separate leg will be present for each waypoint or destination specified. If no waypoints were
// requested, this will contain one element.
//
// See https://developers.google.com/maps/documentation/directions/#Legs
Legs []struct {
// Duration indicates the total duration of this leg.
Duration *Duration `json:"duration"`
// DurationInTraffic indicates the total duration of this leg, taking into account current traffic conditions.
//
// It will only be included if all of the following are true:
// - The directions request includes a DepartureTime parameter set to a value within a few minutes of the current time.
// - The request is made using a Google Maps API for Work client.
// - Traffic conditions are available for the requested route.
// - The directions request doesnot include stopover waypoints.
DurationInTraffic *Duration `json:"duration_in_traffic"`
// Distance is the total distance covered by this leg.
Distance *Distance `json:"distance"`
// ArrivalTime indicates the estimated time of arrival for this leg. This is only included for transit directions.
ArrivalTime Time `json:"arrival_time"`
// DepartureTime indicates the estimated time of arrival for this leg. This is only included for transit directions.
DepartureTime Time `json:"departure_time"`
// StartLocation indicates the origin of this leg.
//
// Because the Directions API calculates directions between locations by using the nearest transportation option (usually a road)
// at the start and end points, StartLocation may be different than the provided origin of this leg if, for example, a road is not near the origin.
StartLocation *LatLng `json:"start_location"`
// EndLocation indicates the destination of this leg.
//
// Because the Directions API calculates directions between locations by using the nearest transportation option (usually a road)
// at the start and end points, EndLocation may be different than the provided destination of this leg if, for example, a road is not near the destination.
EndLocation *LatLng `json:"end_location"`
// StartAddress contains the human-readable address (typically a street address) reflecting the StartLocation of this leg.
StartAddress string `json:"start_address"`
// EndAddress contains the human-readable address (typically a street address) reflecting the EndLocation of this leg.
EndAddress string `json:"end_address"`
// Steps describes each step of the leg of the journey.
//
// See https://developers.google.com/maps/documentation/directions/#Steps
Steps []Step `json:"steps"`
} `json:"legs"`
// Bounds describes the viewport bounding box of the OverviewPolyline
Bounds Bounds `json:"bounds"`
// Copyrights contains the copyright text to be displayed for this route. You must display this information yourself.
Copyrights string `json:"copyrights"`
// OverviewPolyline contains the encoded polyline representation of the route. This polyline is an approximate (smoothed) path of the resulting directions.
OverviewPolyline Polyline `json:"overview_polyline"`
// Warnings contains any warnings to display to the user when showing these directions. You must display these warnings yourself.
Warnings []string `json:"warnings"`
// WaypointOrder indicates the order of any waypoints in the calculated route. The waypoints may be reordered if OptimizeWaypoints was specified.
WaypointOrder []int `json:"waypoint_order"`
}
// Time represents a time.
type Time struct {
// Value indicates the number of seconds since epoch of this time.
Value int64 `json:"value"`
// Text contains a human-readable time, displayed in the time zone of the transit stop.
Text string `json:"text"`
// TimeZone indicates the time zone of this station, e.g., "America/New_York"
TimeZone string `json:"time_zone"`
}
// Time returns the Time as a time.Time, in the specified TimeZone.
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
}
// Step describes a step of a leg of a journey.
//
// See https://developers.google.com/maps/documentation/directions/#Steps
type Step struct {
TravelMode string `json:"travel_mode"` // TODO: not documented? (all caps)
// StartLocation contains the location of the starting point of this step.
StartLocation *LatLng `json:"start_location"`
// EndLocation contains the location of the ending point of this step.
EndLocation *LatLng `json:"end_location"`
Maneuver string `json:"maneuver"` // TODO: not documented?
// Contains an encoded polyline representation of the step. This is an approximate (smoothed) path of the step.
Polyline *Polyline `json:"polyline"`
// Duration indicates the total duration of this step.
Duration *Duration `json:"duration"`
// Distance is the total distance covered by this step.
Distance *Distance `json:"distance"`
// HTMLInstructions contains formatted instructions for this step, presented as an HTML text string.
HTMLInstructions string `json:"html_instructions"`
// Steps contains detailed directions for walking or driving steps in transit directions. These are only available when the requested Mode is ModeTransit
Steps []Step `json:"steps"`
// TransitDetails contains transit-specific information. This is only included when the requested Mode is ModeTransit
//
// See https://developers.google.com/maps/documentation/directions/#TransitDetails
TransitDetails *struct {
// ArrivalStop contains information about the arrival stop for this part of the trip.
ArrivalStop Stop `json:"arrival_stop"`
// DepartureStop contains information about the departure stop for this part of the trip.
DepartureStop Stop `json:"departure_stop"`
// ArrivalTime indicates the arrival time for this leg of the journey.
ArrivalTime Time `json:"arrival_time"`
// DepartureTime indicates the departure time for this leg of the journey.
DepartureTime Time `json:"departure_time"`
// Headsign specifies the direction in which to travel on this line, as it is marked on the vehicle or at the departure stop. This will often be the terminus station.
Headsign string `json:"headsign"`
// Headway specifies the expected number of seconds between departure from the same stop at this time.
//
// For example, with a Headway of 600, you would expect a ten minute way if you should miss your bus.
Headway int64 `json:"headway"` // TODO: to time.Duration?
// NumStops indicates the number of stops in this step, counting the arrival stop, but not the departure stop.
//
// For example, if your directions involve leaving Stop A, passing through Stops B and C, and arriving at Stop D, NumStops will be 3.
NumStops int `json:"num_stops"`
// Line contains information about the transit line used in this step.
Line struct {
// Name is the full name of this transit line, e.g., "7 Avenue Express"
Name string `json:"name"`
// ShortName is the short name of this transit line. This will normally be a line number, such as "M7" or "355"
ShortName string `json:"short_name"`
// Color is the color commonly used in signage for this transit line, specified as a hex string such as "#FF0033"
Color string `json:"color"` // TODO: to image.Color?
// Agencies contains information about the operator of the line.
//
// You must display the names and URLs of the transit agencies servicing the trip results.
Agencies []struct {
// Name is the name of the transit agency.
Name string `json:"name"`
// URL is the URL for the transit agency.
URL string `json:"url"`
// Phone is the phone number of the transit agency.
Phone string `json:"phone"`
} `json:"agencies"`
// URL is the URL for this transit line as provided by the transit agency.
URL string `json:"url"`
// IconURL is the URL for the icon associated with this line.
IconURL string `json:"icon"`
// TextColor is the color of text commonly used for signage of this line, specified as a hex string such as "#FF0033"
TextColor string `json:"text_color"` // TODO: to image.Color?
// Vehicle contains the type of vehicle used on this line.
Vehicle *struct {
// Name is the name of the vehicle used on this line, e.g., "Subway"
Name string `json:"name"`
// Type is the type of the vehicle that runs on this line.
//
// See https://developers.google.com/maps/documentation/directions/#VehicleType
Type string `json:"type"`
// IconURL is the URL for an icon associated with this vehicle type.
IconURL string `json:"icon"`
} `json:"vehicle"`
} `json:"line"`
} `json:"transit_details"`
}
// Stop describes a transit station or stop.
type Stop struct {
// Location is the location of the station/stop.
Location LatLng `json:"location"`
// Name is the name of the transit station/stop, e.g., "Union Square".
Name string `json:"name"`
}
// Bounds describes a viewport bounding box.
type Bounds struct {
// The northeasternmost point of the bounding box.
Northeast LatLng `json:"northeast"`
// The southwesternmost point of the bounding box.
Southwest LatLng `json:"southwest"`
}
func (b Bounds) String() string {
return fmt.Sprintf("%s|%s", b.Northeast, b.Southwest)
}
// Duration describes an amount of time for a leg or step.
type Duration struct {
// Value indicates the duration in seconds.
Value int64 `json:"value"`
// Text contains a human-readable representation of the duration.
Text string `json:"text"`
}
// Duration translates the Duration into a time.Duration.
func (d Duration) Duration() time.Duration {
return time.Duration(d.Value) * time.Second
}
// Distance describes a distance for a leg or step.
type Distance struct {
// Value is the distance in meters
Value int64 `json:"value"`
// Text contains a human-readable representation of the distance, displayed in the requested unit and language, or in the unit and language used at the origin.
Text string `json:"text"`
}
// Polyline contains data describing an encoded polyline.
type Polyline struct {
// Points is an encoded polyline describing some path.
//
// See https://developers.google.com/maps/documentation/utilities/polylinealgorithm
Points string `json:"points"`
}
// TODO: methods to decode polyline points?
package maps
import (
"fmt"
"net/url"
"time"
"golang.org/x/net/context"
)
// DistanceMatrix requests travel distance and time for a matrix of origins and destinations.
//
// See https://developers.google.com/maps/documentation/distancematrix/
func DistanceMatrix(ctx context.Context, orig, dest []Location, opts *DistanceMatrixOpts) (*DistanceMatrixResult, error) {
var d distanceResponse
if err := doDecode(ctx, baseURL+distancematrix(orig, dest, opts), &d); err != nil {
return nil, err
}
if d.Status != StatusOK {
return nil, APIError{d.Status, ""}
}
return &d.DistanceMatrixResult, nil
}
func distancematrix(orig, dest []Location, opts *DistanceMatrixOpts) string {
p := url.Values{}
p.Set("origins", encodeLocations(orig))
p.Set("destinations", encodeLocations(dest))
opts.update(p)
return "distancematrix/json?" + p.Encode()
}
// DistanceMatrixOpts defines options for DistanceMatrix requsts.
type DistanceMatrixOpts struct {
// The language in which to return results.
//
// See https://developers.google.com/maps/faq#languagesupport
Language string
// Specifies the unit system to use when displaying results.
//
// Accepted values are UnitMetric and UnitImperial.
//
// See https://developers.google.com/maps/documentation/distancematrix/#unit_systems
Units string
// Specifies the mode of transport to use when calculating directions.
//
// Accepted values are ModeDriving (the default), ModeWalking and ModeBicycling.
Mode string
// Indicates that the calculated route(s) should avoid the indicated features.
//
// Accepted values are AvoidTolls, AvoidHighways or AvoidFerries
//
// See https://developers.google.com/maps/documentation/distancematrix/#Restrictions
Avoid string
// Specifies the desired time of departure.
//
// The departure time can be specified for Google Maps API for Work clients to receive trip duration considering current traffic conditions.
DepartureTime time.Time
}
func (o *DistanceMatrixOpts) update(p url.Values) {
if o == nil {
return
}
if o.Mode != "" {
p.Set("mode", o.Mode)
}
if o.Language != "" {
p.Set("language", o.Language)
}
if o.Avoid != "" {
p.Set("avoid", o.Avoid)
}
if o.Units != "" {
p.Set("units", o.Units)
}
if !o.DepartureTime.IsZero() {
p.Set("departure_time", fmt.Sprintf("%d", o.DepartureTime.Unix()))
}
}
type distanceResponse struct {
Status string `json:"status"`
DistanceMatrixResult
}
// DistanceMatrixResult describes the matrix of distances between the requested origins and destinations.
type DistanceMatrixResult struct {
// OriginAddresses contains the addresses returned by the API from your original request.
//
// These are formatted by the geocoder and localized according to the requested Language.
OriginAddresses []string `json:"origin_addresses"`
// DestinationAddresses contains the addresses returned by the API from your original request.
//
// These are formatted by the geocoder and localized according to the requested Language.
DestinationAddresses []string `json:"destination_addresses"`
// Rows describes rows in the matrix.
Rows []struct {
// Elements describes columns in the matrix.
Elements []struct {
// Status indicates the status of the request, and will be one of StatusOK, StatusNotFound or StatusZeroResults.
Status string `json:"status"`
// Duration indicates the total duration of this journey.
Duration Duration `json:"duration"`
// Distance indicates the total distance of this journey.
Distance Distance `json:"distance"`
} `json:"elements"`
} `json:"rows"`
}
package maps
import (
"fmt"
"net/url"
"golang.org/x/net/context"
)
// Elevation requests elevation data for a series of locations.
//
// See https://developers.google.com/maps/documentation/elevation/
func Elevation(ctx context.Context, ll []LatLng) ([]ElevationResult, error) {
var r elevationResponse
if err := doDecode(ctx, baseURL+elevation(ll), &r); err != nil {
return nil, err
}
if r.Status != StatusOK {
return nil, APIError{r.Status, ""}
}
return r.Results, nil
}
// ElevationPolyline requests elevation data for a series of locations as specified as an encoded polyline.
//
// See https://developers.google.com/maps/documentation/elevation/#Locations
func ElevationPolyline(ctx context.Context, p string) ([]ElevationResult, error) {
var r elevationResponse
if err := doDecode(ctx, baseURL+elevationpoly(p), &r); err != nil {
return nil, err
}
if r.Status != StatusOK {
return nil, APIError{r.Status, ""}
}
return r.Results, nil
}
// ElevationPath requests elevation data for a number of samples along a path described as a series of locations.
func ElevationPath(ctx context.Context, ll []LatLng, samples int) ([]ElevationResult, error) {
var r elevationResponse
if err := doDecode(ctx, baseURL+elevationpath(ll, samples), &r); err != nil {
return nil, err
}
if r.Status != StatusOK {
return nil, APIError{r.Status, ""}
}
return r.Results, nil
}
// ElevationPathPoly requests elevation data for a number of samples along a path described as a series of locations specified as an encoded polyline.
func ElevationPathPoly(ctx context.Context, p string, samples int) ([]ElevationResult, error) {
var r elevationResponse
if err := doDecode(ctx, baseURL+elevationpathpoly(p, samples), &r); err != nil {
return nil, err
}
if r.Status != StatusOK {
return nil, APIError{r.Status, ""}
}
return r.Results, nil
}
func elevation(ll []LatLng) string {
p := url.Values{}
p.Set("locations", encodeLatLngs(ll))
return "elevation/json?" + p.Encode()
}
func elevationpoly(poly string) string {
p := url.Values{}
p.Set("locations", "enc:"+poly)
return "elevation/json?" + p.Encode()
}
func elevationpath(ll []LatLng, s int) string {
p := url.Values{}
p.Set("path", encodeLatLngs(ll))
p.Set("samples", fmt.Sprintf("%d", s))
return "elevation/json?" + p.Encode()
}
func elevationpathpoly(poly string, s int) string {
p := url.Values{}
p.Set("path", "enc:"+poly)
p.Set("samples", fmt.Sprintf("%d", s))
return "elevation/json?" + p.Encode()
}
type elevationResponse struct {
Results []ElevationResult `json:"results"`
Status string `json:"status"`
}
// ElevationResult describes elevation data.
type ElevationResult struct {
// Elevation is elevation of the location in meters.
Elevation float64 `json:"elevation"`
// Location is the location of the location being described.
Location LatLng `json:"location"`
// Resolution is the maximum distance between data points from which the elevation was interpolated, in meters.
//
// This property will be zero if the resolution is not known. Note that elevation data becomes
// more coarse (larger Resolution values) when multiple points are passed. To obtain the most
// accurate elevation value for a point, it should be queried independently.
Resolution float64 `json:"resolution"`
}
package maps
import (
"fmt"
"net/url"
"strings"
"golang.org/x/net/context"
)
const (
// ComponentRoute matches long or short name of a route.
ComponentRoute = "route"
// ComponentLocality matches against most locality and sublocality types.
ComponentLocality = "locality"
// ComponentAdministrativeArea matches all the administrative area levels.
ComponentAdministrativeArea = "administrative_area"
// ComponentPostalCode matches postcal code and postal code prefix.
ComponentPostalCode = "postal_code"
// ComponentCountry matches a country name or a two-letter ISO 3166-1 country code.
ComponentCountry = "country"
// LocationTypeRooftop restricts the results to addresses for which we have location information accurate down to street address precision.
LocationTypeRooftop = "ROOFTOP"
// LocationTypeRangeInterpolated restricts the results to those that reflect an approximation (usually a road) interpolated between two precise points (such as intersections).
LocationTypeRangeInterpolated = "RANGE_INTERPOLATED"
// LocationTypeGeometricCenter restricts the results to geometric centers of a location such as a polyline (for example, a street) or polygon (region).
LocationTypeGeometricCenter = "GEOMETRIC_CENTER"
// LocationTypeApproximate restricts the results to those that are characterized as approximate.
LocationTypeApproximate = "APPROXIMATE"
// TODO: enums for https://developers.google.com/maps/documentation/geocoding/#Types
)
// Geocode requests conversion of an address to a latitude/longitude pair.
func Geocode(ctx context.Context, opts *GeocodeOpts) ([]GeocodeResult, error) {
var r geocodeResponse
if err := doDecode(ctx, baseURL+geocode(opts), &r); err != nil {
return nil, err
}
if r.Status != StatusOK {
return nil, APIError{r.Status, ""}
}
return r.Results, nil
}
func geocode(opts *GeocodeOpts) string {
p := url.Values{}
opts.update(p)
return "geocode/json?" + p.Encode()
}
// GeocodeOpts defines options for Geocode requests.
type GeocodeOpts struct {
// The address to geocode.
Address Address
// Components specifies a component filter for which you wish to obtain a geocode.
//
// Only results that match all filters will be returned. Filter values support the same methods of spelling correction and partial matching as other geocoding requests.
//
// See https://developers.google.com/maps/documentation/geocoding/#ComponentFiltering
Components []Component
// The language in which to return results.
//
// See https://developers.google.com/maps/faq#languagesupport
Language string
// Region specifies a region to bias results toward, specified as a ccTLD ("top-level domain" two-character value.
//
// This will influence, not fully restrict, results from the geocoder.
//
// See https://developers.google.com/maps/documentation/geocoding/#RegionCodes
Region string
// Bounds specifies the bounding box of the viewport within which to bias geocode results more prominently.
//
// This will influence, not fully restrict, results from the geocoder.
Bounds *Bounds
}
// Component describes a single component filter.
//
// See https://developers.google.com/maps/documentation/geocoding/#ComponentFiltering
type Component struct {
// Key is the type of component to filter on.
//
// Accepted values are ComponentRoute, ComponentLocality, ComponentAdministrativeArea, ComponentPostalCode and ComponentCountry.
Key string
// Value is the value of the filter to enforce.
Value string
}
func (c Component) encode() string {
return fmt.Sprintf("%s:%s", c.Key, c.Value)
}
func encodeComponents(cs []Component) string {
s := make([]string, len(cs))
for i, c := range cs {
s[i] = c.encode()
}
return strings.Join(s, "|")
}
func (g *GeocodeOpts) update(p url.Values) {
if g == nil {
return
}
if string(g.Address) != "" {
p.Set("address", g.Address.Location())
}
if g.Components != nil {
p.Set("components", encodeComponents(g.Components))
}
if g.Language != "" {
p.Set("language", g.Language)
}
if g.Region != "" {
p.Set("region", g.Region)
}
if g.Bounds != nil {
p.Set("bounds", g.Bounds.String())
}
}
type geocodeResponse struct {
Results []GeocodeResult `json:"results"`
Status string `json:"status"`
}
// GeocodeResult represents the geocoded result for the given input.
type GeocodeResult struct {
// AddressComponents contains the separate address components.
AddressComponents []struct {
// LongName is the full text description or name of the address component returned by the geocoder.
LongName string `json:"long_name"`
// ShortName is the abbreviated textual name for the address component, if available.
//
// For example, an address component for the state of Alaska may have a LongName of "Alaska" and a ShortName of "AK" using the 2-letter postal abbreviation.
ShortName string `json:"short_name"`
// Types contains the types of address components.
Types []string `json:"types"`
} `json:"address_components"`
// PostcodeLocalities denotes all the localities contained in a postal code.
//
// This is only present when the result is a postal code that contains multiple localities.
PostcodeLocalities []string `json:"postcode_localities"`
// FormattedAddress contains the human-readable address of this location.
//
// Often this is equivalent to the "postal address" which sometimes differs from country to country.
FormattedAddress string `json:"formatted_address"`
// Geometry contains information about the location.
Geometry struct {
// Location is the geocoded latitude and longitude of the location.
Location LatLng `json:"location"`
// LocationType specifies additional data about the specified location.
//
// Its value will be one of:
// - "ROOFTOP" indicating that the returned result is a precise geocode for which we have location information accurate down to street address precision.
// - "RANGE_INTERPOLATED" indicating that the returned result reflects and approximation (usually on a road) interpolated between two precise points (such as intersections). Interpolated results are generally returned when rooftop geocodes are unavailable for a street address.
// - "GEOMETRIC_CENTER" indicating that the returned result is the geometric center of a result such as a polyline (for example, a street) or polygon (region).
LocationType string `json:"location_type"`
// Viewport contains the recommended viewport bounding box for displaying the returned result.
Viewport Bounds `json:"viewport"`
// Bounds, if included, contains the bounding box which can fully contain the returned result.
//
// Note that this may not match Viewport.
Bounds Bounds `json:"bounds"`
} `json:"geometry"`
// Types indicates the types of the returned result.
//
// This contains a set of zero or more tags identifying the type of feature returned in a result.
// For example, a geocode of "Chicago" returns "locality" which indicates that "Chicago" is a city,
// and also returns "political" which indicates it is a political entity.
Types []string `json:"types"`
// PartialMatch indicates that the geocoder did not return an exact match for the original request,
// though it was able to match part of the requested address. You may wish to examine the original
// request for misspellings and/or an incomplete address.
PartialMatch string `json:"partial_match"`
}
// ReverseGeocode requests conversion of a location to its nearest address.
//
// See https://developers.google.com/maps/documentation/geocoding/#ReverseGeocoding
func ReverseGeocode(ctx context.Context, ll LatLng, opts *ReverseGeocodeOpts) ([]GeocodeResult, error) {
var r geocodeResponse
if err := doDecode(ctx, baseURL+reversegeocode(ll, opts), &r); err != nil {
return nil, err
}
if r.Status != StatusOK {
return nil, APIError{r.Status, ""}
}
return r.Results, nil
}
// ReverseGeocodeOpts defines options for ReverseGeocode requests.
type ReverseGeocodeOpts struct {
// The language in which to return results.
//
// See https://developers.google.com/maps/faq#languagesupport
Language string
// ResultTypes specifies filters on result types, e.g., "country", "street address"
ResultTypes []string
// LocationTypes specifies filters on location types.
//
// Accepted values are LocationTypeRooftop, LocationTypeRangeInterpolated, LocationTypeGeometricCenter and LocationTypeApproximate.
LocationTypes []string
}
func (r *ReverseGeocodeOpts) update(p url.Values) {
if r == nil {
return
}
if r.ResultTypes != nil {
p.Set("result_type", strings.Join(r.ResultTypes, "|"))
}
if r.LocationTypes != nil {
p.Set("location_type", strings.Join(r.LocationTypes, "|"))
}
}
func reversegeocode(ll LatLng, opts *ReverseGeocodeOpts) string {
p := url.Values{}
p.Set("latlng", ll.String())
opts.update(p)
return "geocode/json?" + p.Encode()
}
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "{}"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright {yyyy} {name of copyright owner}
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
package maps
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"encoding/json"
"fmt"
"net/http"
"strings"
"golang.org/x/net/context"
)
const (
baseURL = "https://maps.googleapis.com/maps/api/"
// StatusOK indicates the response contains a valid result.
StatusOK = "OK"
// StatusNotFound indicates at least one of the locations specified could not be geocoded.
StatusNotFound = "NOT_FOUND"
// StatusZeroResults indicates no route could be found between the origin and destination.
StatusZeroResults = "ZERO_RESULTS"
// StatusMaxWaypointsExceeded indicates that too many Waypoints were provided in the request.
//
// The maximum allowed waypoints is 8, plus the origin and destination. Google Maps API for Work clients may contain requests with up to 23 waypoints.
StatusMaxWaypointsExceeded = "MAX_WAYPOINTS_EXCEEDED"
// StatusInvalidRequest indicates that the provided request was invalid.
StatusInvalidRequest = "INVALID_REQUEST"
// StatusRequestDenied indicates that the service denied use of the service by your application.
StatusRequestDenied = "REQUEST_DENIED"
// StatusUnknownError indicates that the request could not be processed due to a server error. The request may succeed if you try again.
StatusUnknownError = "UNKNOWN_ERROR"
// StatusOverQueryLimit indicates that the service has received too many requests from your application within the allowed time period.
StatusOverQueryLimit = "OVER_QUERY_LIMIT"
)
func do(ctx context.Context, url string) (*http.Response, error) {
cl := httpClient(ctx)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
q := req.URL.Query()
if k := key(ctx); k != "" {
q.Set("key", k)
}
clientID, privKey := workCreds(ctx)
if clientID != "" {
q.Set("client", clientID)
}
enc := q.Encode()
if privKey != "" {
sig, err := genSig(privKey, req.URL.Path, enc)
if err != nil {
return nil, err
}
enc += "&signature=" + sig
}
req.URL.RawQuery = enc
return cl.Do(req)
}
// See https://developers.google.com/maps/documentation/business/webservices/auth
func genSig(privKey, path, query string) (string, error) {
toSign := path + "?" + query
decodedKey, err := base64.URLEncoding.DecodeString(privKey)
if err != nil {
return "", err
}
d := hmac.New(sha1.New, decodedKey)
if _, err := d.Write([]byte(toSign)); err != nil {
return "", err
}
return base64.URLEncoding.EncodeToString(d.Sum(nil)), nil
}
func doDecode(ctx context.Context, url string, r interface{}) error {
resp, err := do(ctx, 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
}
// HTTPError indicates an error communicating with the API server, and includes the HTTP response returned from the server.
type HTTPError struct {
// Response is the http.Response returned from the API request.
Response *http.Response
}
func (e HTTPError) Error() string {
return fmt.Sprintf("http error %d", e.Response.StatusCode)
}
// APIError indicates a failure response from the API server, even though a successful HTTP response was returned.
// Its Status and Message fields can be consulted for more information about the specific error conditions.
type APIError struct {
Status, Message string
}
func (e APIError) Error() string {
if e.Message != "" {
return fmt.Sprintf("API error %q: %s", e.Status, e.Message)
}
return fmt.Sprintf("API Error %q", e.Status)
}
// Location represents the general concept of a location in various methods.
type Location interface {
Location() string
}
// LatLng represents a Location that is identified by its longitude and latitude.
type LatLng struct {
Lat float64 `json:"lat"`
Lng float64 `json:"lng"`
}
// Location returns the latitude/longitude pair as a comma-separated string.
func (ll LatLng) Location() string {
return fmt.Sprintf("%f,%f", ll.Lat, ll.Lng)
}
func (ll LatLng) String() string {
return ll.Location()
}
// Address represents a Location that is identified by its name or address, e.g., "New York, NY" or "111 8th Ave, NYC"
type Address string
// Location returns the Address as a string.
func (a Address) Location() string {
return string(a)
}
func encodeLocations(ls []Location) string {
s := make([]string, len(ls))
for i, l := range ls {
s[i] = l.Location()
}
return strings.Join(s, "|")
}
func encodeLatLngs(ll []LatLng) string {
s := make([]string, len(ll))
for i, l := range ll {
s[i] = l.Location()
}
return strings.Join(s, "|")
}
// Float64 returns a pointer to the given float64 value.
//
// This is a convenience method since some options structs take a *float64.
func Float64(f float64) *float64 {
return &f
}
package maps
import (
"flag"
"image/color"
"net/http"
"testing"
"time"
)
var (
apiKey = flag.String("apiKey", "", "Google Maps API key")
ctx = NewContext("", &http.Client{})
wctx = NewWorkContext("clientID", "privKey", &http.Client{})
)
func init() {
flag.Parse()
}
func TestDirections(t *testing.T) {
orig, dest := Address("111 8th Ave, NYC"), Address("170 E 92nd St, NYC")
opts := &DirectionsOpts{
Mode: ModeTransit,
DepartureTime: time.Now(),
Alternatives: true,
}
t.Logf("%s", baseURL+directions(orig, dest, opts))
r, err := Directions(ctx, orig, dest, opts)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
t.Logf("%v", r)
}
func TestStaticMap(t *testing.T) {
s := Size{512, 512}
opts := &StaticMapOpts{
Center: LatLng{-5, -5},
Markers: []Markers{
{
Size: "small",
Color: color.Black,
Locations: []Location{
LatLng{1, 1},
LatLng{2, 2},
},
}, {
Size: "mid",
Color: color.White,
Locations: []Location{
LatLng{3, 3},
},
},
},
Paths: []Path{
{
Weight: 10,
Color: color.Black,
Locations: []Location{
LatLng{4, 4},
LatLng{5, 5},
},
}, {
Color: color.Black,
FillColor: color.White,
Locations: []Location{
LatLng{6, 6}, LatLng{7, 7}, LatLng{7, 3},
},
},
},
Visible: []Location{
LatLng{15, 15},
},
Styles: []Style{
{
Feature: "water",
Element: "geometry.fill",
Rules: []StyleRule{
{Hue: color.White, Saturation: -100.0},
},
},
},
}
t.Logf("%s", baseURL+staticmap(s, opts))
if _, err := StaticMap(ctx, s, opts); err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestStreetView(t *testing.T) {
s := Size{600, 300}
opts := &StreetViewOpts{
Location: &LatLng{46.414382, 10.013988},
Heading: Float64(151.78),
Pitch: -0.76,
}
t.Logf("%s", baseURL+streetview(s, opts))
if _, err := StreetView(ctx, s, opts); err != nil {
t.Errorf("unexpected error: %v", err)
}
}
func TestTimeZone(t *testing.T) {
ll := LatLng{40.7142700, -74.0059700}
tm := time.Now()
t.Logf("%s", baseURL+timezone(ll, tm, nil))
r, err := TimeZone(ctx, ll, tm, nil)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
t.Logf("%v", r)
}
func TestElevation(t *testing.T) {
ll := []LatLng{{39.7391536, -104.9847034}}
t.Logf("%s", baseURL+elevation(ll))
r, err := Elevation(ctx, ll)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
t.Logf("%v", r)
p := "gfo}EtohhU"
t.Logf("%s", baseURL+elevationpoly(p))
r, err = ElevationPolyline(ctx, p)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
t.Logf("%v", r)
samples := 3
ll = []LatLng{{36.578581, -118.291994}, {36.23998, -116.83171}}
t.Logf("%s", baseURL+elevationpath(ll, samples))
r, err = ElevationPath(ctx, ll, samples)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
t.Logf("%v", r)
p = "gfo}EtohhUxD@bAxJmGF"
t.Logf("%s", baseURL+elevationpathpoly(p, samples))
r, err = ElevationPathPoly(ctx, p, samples)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
t.Logf("%v", r)
}
func TestGeocode(t *testing.T) {
opts := &GeocodeOpts{
Address: Address("1600 Amphitheatre Parkway, Mountain View, CA"),
}
t.Logf("%s", baseURL+geocode(opts))
r, err := Geocode(ctx, opts)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
t.Logf("%v", r)
ll := LatLng{40.714224, -73.961452}
t.Logf("%s", baseURL+reversegeocode(ll, nil))
r, err = ReverseGeocode(ctx, ll, nil)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
t.Logf("%v", r)
}
func TestDistanceMatrix(t *testing.T) {
orig := []Location{Address("Vancouver, BC"), Address("Seattle")}
dst := []Location{Address("San Francisco"), Address("Victoria, BC")}
opts := &DistanceMatrixOpts{
Mode: ModeBicycling,
Language: "fr-FR",
}
t.Logf("%s", baseURL+distancematrix(orig, dst, opts))
r, err := DistanceMatrix(ctx, orig, dst, opts)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
t.Logf("%v", r)
}
func TestSnapToRoads(t *testing.T) {
path := []LatLng{{-35.27801, 149.12958},
{-35.28032, 149.12907},
{-35.28099, 149.12929},
{-35.28144, 149.12984},
{-35.28194, 149.13003},
{-35.28282, 149.12956},
{-35.28302, 149.12881},
{-35.28473, 149.12836}}
opts := &SnapToRoadsOpts{
Interpolate: true,
}
if _, err := SnapToRoads(ctx, path, opts); err != ErrNoAPIKey {
t.Errorf("unexpected error, got %v, want %v", err, ErrNoAPIKey)
}
keyCtx := NewContext(*apiKey, &http.Client{})
r, err := SnapToRoads(keyCtx, path, opts)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
t.Logf("%v", r)
}
// Based on https://developers.google.com/maps/documentation/business/webservices/auth#signature_examples
func TestSignature(t *testing.T) {
clientID := "clientID"
privateKey := "vNIXE0xscrmjlyV-12Nj_BvUPaw="
sig, err := genSig(privateKey, "/maps/api/geocode/json", "address=New+York&client="+clientID)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
exp := "chaRF2hTJKOScPr-RQCEhZbSzIE="
if sig != exp {
t.Errorf("unexpected signature, got %q, want %q", sig, exp)
}
ctx := NewWorkContext(clientID, privateKey, nil)
if gcid, gkey := workCreds(ctx); gcid != clientID || gkey != privateKey {
t.Errorf("unepxected credentials from context, got %q and %q, want %q and %q", gcid, gkey, clientID, privateKey)
}
}
package maps
import (
"errors"
"net/url"
"code.google.com/p/go.net/context"
)
const roadsAPIBaseURL = "https://roads.googleapis.com/v1/"
var ErrNoAPIKey = errors.New("must provide an API key")
func SnapToRoads(ctx context.Context, path []LatLng, opts *SnapToRoadsOpts) ([]SnappedPoint, error) {
if key(ctx) == "" {
return nil, ErrNoAPIKey
}
var d snapToRoadsResponse
if err := doDecode(ctx, roadsAPIBaseURL+snapToRoads(path, opts), &d); err != nil {
return nil, err
}
return d.SnappedPoints, nil
}
func snapToRoads(path []LatLng, opts *SnapToRoadsOpts) string {
p := url.Values{}
p.Set("path", encodeLatLngs(path))
opts.update(p)
return "snapToRoads?" + p.Encode()
}
// SnapToRoadsOpts defines options for SnapToRoads requests.
type SnapToRoadsOpts struct {
// Whether to interpolate a path to include all points forming the full road-geometry.
Interpolate bool
}
func (o SnapToRoadsOpts) update(p url.Values) {
if o.Interpolate {
p.Set("interpolate", "true")
}
}
type snapToRoadsResponse struct {
SnappedPoints []SnappedPoint `json:"snappedPoints"`
}
// SnappedPoint represents a location derived from a SnapToRoads request.
type SnappedPoint struct {
// The latitude and longitude of the snapped location.
Location SnappedLocation `json:"location"`
// The index of the corresponding value in the original request.
//
// Each value in the request should map to a snapped value in the response. However, if you've set Interpolate to be true, then it's possible that the response will contain more coordinates than the request. Interpolated values will have this value set to nil.
OriginalIndex *int `json:"originalIndex,omitempty"`
// A unique identifier for a Place corresponding to a road segment.
PlaceID string `json:"placeId"`
}
// SnappedLocation is equivalent to LatLng
//
// TODO(jasonhall): Reconcile this with LatLng which expects JSON fields lat/lng
type SnappedLocation struct {
Latitude float64 `json:"latitude"`
Longitude float64 `json:"longitude"`
}
package maps
import (
"fmt"
"image/color"
"io"
"net/http"
"net/url"
"strings"
"golang.org/x/net/context"
)
const (
// MapFormatPNG requests the image in PNG format.
MapFormatPNG = "png"
// MapFormatPNG32 requests the image in PNG32 format.
MapFormatPNG32 = "png32"
// MapFormatGIF requests the image in GIF format.
MapFormatGIF = "gif"
// MapFormatJPG requests the image in JPG format.
MapFormatJPG = "jpg"
// MapFormatJPGBaseline requests the image in a non-progressive JPG format.
MapFormatJPGBaseline = "jpg-baseline"
// MapTypeRoadmap requests a standard roadmap image.
MapTypeRoadmap = "roadmap"
// MapTypeSatellite requests a satellite image.
MapTypeSatellite = "satellite"
// MapTypeTerrain requests a terrain image.
MapTypeTerrain = "terrain"
// MapTypeHybrid requests a hybrid of the satellite and roadmap image.
MapTypeHybrid = "hybrid"
// SizeTiny requests a small-sized marker.
SizeTiny = "tiny"
// SizeMid requests a mid-sized marker.
SizeMid = "mid"
// SizeLarge requests a large-sized marker.
SizeLarge = "large"
// VisibilityOn requests that the feature be shown.
VisibilityOn = "on"
// VisibilityOff requests that the feature not be shown.
VisibilityOff = "off"
// VisibilitySimplified requests that the feature be shown in a simplified manner.
VisibilitySimplified = "simplified"
)
// StaticMap requests a static map image of a requested size.
func StaticMap(ctx context.Context, s Size, opts *StaticMapOpts) (io.ReadCloser, error) {
resp, err := do(ctx, baseURL+staticmap(s, opts))
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, HTTPError{resp}
}
return resp.Body, nil
}
func staticmap(s Size, opts *StaticMapOpts) string {
p := url.Values{}
p.Set("size", s.String())
opts.update(p)
return "staticmap?" + p.Encode()
}
// Size specifies the height and width of the image to request in pixels.
//
// See https://developers.google.com/maps/documentation/staticmaps/#Imagesizes
type Size struct {
H, W int
}
func (s Size) String() string {
return fmt.Sprintf("%dx%d", s.W, s.H)
}
// StaticMapOpts defines options for StaticMap requests.
type StaticMapOpts struct {
// Center specifies the location to place at the center of the image.
Center Location
// Zoom is the zoom level to request.
//
// The zoom level is between 0 (the lowest level, in which the entire world can be seen on one map)
// and 21+ (down to streets and individual buildings).
//
// Each zoom level doubles the precision in both horizontal and vertical dimensions.
//
// See https://developers.google.com/maps/documentation/staticmaps/#Zoomlevels
Zoom int
// Scale requests a higher-resolution map image for use on high-density displays.
//
// A scale value of 2 will double the resulting image size. A scale value of 4 (only available to Google Maps API for Work clients) will quadruple it.
Scale int
// Format specifies the image format to request.
//
// Accepted values are MapFormatPNG (the default), MapFormatPNG32, MapFormatGIF, MapFormatJPG and MapFormatJPGBaseline.
Format string
// MapType specifies the map type to request.
//
// Accepted values are MapTypeRoadmap (the default), MapTypeSatellite, MapTypeTerrain and MapTypeHybrid.
MapType string
// The language in which to localize labels on the map.
//
// See https://developers.google.com/maps/faq#languagesupport
Language string
// Region defines the appropriate borders to display, based on geo-political sensitivities.
//
// Accepts a two-character ccTLD ("top-level domain") value.
Region string
// Markers defines one or more markers to attach to the image at specified locations.
//
// See https://developers.google.com/maps/documentation/staticmaps/#Markers
Markers []Markers
// Path defines one or more paths to attach to the image at specified locations.
//
// See https://developers.google.com/maps/documentation/staticmaps/#Paths
Paths []Path
// Visible specifies one or more locations that should remain visible on the map, though no markers or other indicators will be displayed.
Visible []Location
// Styles defines custom styles to alter the presentation of specific features on the map.
//
// See https://developers.google.com/maps/documentation/staticmaps/#StyledMaps
Styles []Style
}
// Markers defines marker(s) to attach to the image.
type Markers struct {
// Size defines the size of the marker(s).
//
// Accepted values are SizeTiny, SizeMid (the default) and SizeLarge.
Size string // tiny, mid, small
// Color defines the color of the marker(s).
//
// The alpha value of this color is ignored.
Color color.Color
// Label specifies a single uppercase alphanumeric character to place inside the marker image.
Label string
// IconURL specifies the URL of a custom icon image to use.
IconURL string
// HideShadow, if true, will not include a shadow for the marker(s).
HideShadow bool
// Locations specifies the locations of markers to be placed in this group.
Locations []Location
}
func rgb(c color.Color) string {
r, g, b, _ := c.RGBA()
return fmt.Sprintf("0x%02X%02X%02X", r>>8, g>>8, b>>8)
}
func rgba(c color.Color) string {
r, g, b, a := c.RGBA()
return fmt.Sprintf("0x%02X%02X%02X%02X", r>>8, g>>8, b>>8, a>>8)
}
func (m Markers) encode() string {
s := []string{}
if m.Size != "" {
s = append(s, "size:"+m.Size)
}
if m.Color != nil {
s = append(s, "color:"+rgb(m.Color))
}
if m.Label != "" {
s = append(s, "label:"+m.Label)
}
if m.IconURL != "" {
s = append(s, "icon:"+m.IconURL)
}
if m.HideShadow {
s = append(s, "shadow:false")
}
style := strings.Join(s, "|")
if style != "" {
style += "|"
}
return style + encodeLocations(m.Locations)
}
// Path defines a path to attach to the image at specified locations.
type Path struct {
// Weight specifies the thickness of the path in pixels. If no weight is specified, the default is 5 pixels.
Weight int
// Color defines the color of the path.
Color color.Color
// FillColor defines the color to fill the area of the path.
FillColor color.Color
// Geodesic, if true, indicates that the requested path should be interpreted as a geodesic line that follows the curvature of the earth.
//
// If false (the default), the path is rendered as a straight line in screen space.
Geodesic bool
// Polyline specifies an encoded polyline of points defining the path, if Locations is not provided.
Polyline string
// Locations specifies the points of the path.
Locations []Location
}
func (p Path) encode() string {
s := []string{}
if p.Weight != 0 {
s = append(s, fmt.Sprintf("weight:%d", p.Weight))
}
if p.Color != nil {
s = append(s, "color:"+rgba(p.Color))
}
if p.FillColor != nil {
s = append(s, "fillcolor:"+rgba(p.FillColor))
}
if p.Geodesic {
s = append(s, "geodesic:true")
}
style := strings.Join(s, "|")
if style != "" {
style += "|"
}
if p.Polyline != "" {
return style + "enc:" + p.Polyline
}
return style + encodeLocations(p.Locations)
}
// Style defines a set of rules to use to style the requested map image.
type Style struct {
// Feature specifies the feature type for this style modification.
//
// See https://developers.google.com/maps/documentation/staticmaps/#StyledMapFeatures
Feature string // TODO enum
// Element indicates the subset of selected features to style.
//
// See https://developers.google.com/maps/documentation/staticmaps/#StyledMapElements
Element string // TODO enum
// Rules specifies the style rules to apply to the map.
Rules []StyleRule
}
func (t Style) encode() string {
s := []string{}
if t.Feature != "" {
s = append(s, "feature:"+t.Feature)
}
if t.Element != "" {
s = append(s, "element:"+t.Element)
}
for _, r := range t.Rules {
s = append(s, r.encode())
}
return strings.Join(s, "|")
}
// StyleRule defines a style rule to apply to the map.
type StyleRule struct {
// Hue is the hue value to apply to the selection.
//
// Note that while this takes a color value, it only uses this value to determine the basic color (its orientation around the color wheel),
// not its saturation or lightness, which are indicated separately as percentage changes.
Hue color.Color
// Lightness (a value between -100 and 100) indicates the percentage change in brightness of the element. -100 is black, 100 is white.
Lightness float64
// Saturation (a value between -100 and 100) indicates the percentage change in intensity of the basic color to apply to the element.
Saturation float64
// Gamma (a value between 0.01 and 10.0, where 1.0 applies no correction) indicates the amount of gamma correction to apply to the element.
Gamma *float64 // .01 to 10, default 1
// InverseLightness, if true, inverts the Lightness value.
InverseLightness bool
// Visibility indicates whether and how the element appears on the map.
//
// Accepted values are VisibilityOn (the default), VisibilityOff and VisibilitySimplified.
Visibility string
}
func (r StyleRule) encode() string {
s := []string{}
if r.Hue != nil {
s = append(s, "hue:"+rgb(r.Hue))
}
if r.Lightness != 0 {
s = append(s, fmt.Sprintf("lightness:%f", r.Lightness))
}
if r.Saturation != 0 {
s = append(s, fmt.Sprintf("saturation:%f", r.Saturation))
}
if r.Gamma != nil {
s = append(s, fmt.Sprintf("gamma:%f", *r.Gamma))
}
if r.InverseLightness {
s = append(s, "inverse_lightness:true")
}
if r.Visibility != "" {
s = append(s, "visibility:"+r.Visibility)
}
return strings.Join(s, "|")
}
func (so *StaticMapOpts) update(p url.Values) {
if so == nil {
return
}
if so.Center != nil {
p.Set("center", so.Center.Location())
}
if so.Zoom != 0 {
p.Set("zoom", fmt.Sprintf("%d", so.Zoom))
}
if so.Scale != 0 {
p.Set("scale", fmt.Sprintf("%d", so.Scale))
}
if so.Format != "" {
p.Set("format", so.Format)
}
if so.MapType != "" {
p.Set("maptype", so.MapType)
}
if so.Language != "" {
p.Set("language", so.Language)
}
if so.Region != "" {
p.Set("region", so.Region)
}
for _, m := range so.Markers {
p.Add("markers", m.encode())
}
for _, path := range so.Paths {
p.Add("path", path.encode())
}
if so.Visible != nil {
p.Set("visible", encodeLocations(so.Visible))
}
for _, s := range so.Styles {
p.Add("style", s.encode())
}
}
package maps
import (
"fmt"
"io"
"net/http"
"net/url"
"golang.org/x/net/context"
)
// StreetView requests a static StreetView image of the requested size.
func StreetView(ctx context.Context, s Size, opts *StreetViewOpts) (io.ReadCloser, error) {
resp, err := do(ctx, baseURL+streetview(s, opts))
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, HTTPError{resp}
}
return resp.Body, nil
}
func streetview(s Size, opts *StreetViewOpts) string {
p := url.Values{}
p.Set("size", s.String())
opts.update(p)
return "streetview?" + p.Encode()
}
// StreetViewOpts defines options for StreetView requests.
type StreetViewOpts struct {
// Location specifies the location where the image should be snapped from.
Location Location
// Pano specifies a specific Panoramio ID.
Pano string
// Heading specifies the compass heading (between 0 and 360) of the camera.
//
// Both 0 and 360 indicate North, 90 indicates East, 180 indicates South, and so on.
//
// If no heading is specified, a value will be calculated that directs the camera towards the specified Location, from a point at which the closest photograph was taken.
Heading *float64
// FOV specifies the field of view of the image.
//
// Accepted values are between 0 and 120, and if no value is given, a field of view of 90 degrees will be used.
FOV *float64
// Pitch specifies the up or down angle of the camera relative to the Street View vehicle.
//
// This is often, but not always, flat horizontal. Positive values angle the camera up (90 degrees indicating straight up); negative values angle the camera down (-90 indicates straight down).
Pitch float64
}
func (s *StreetViewOpts) update(p url.Values) {
if s == nil {
return
}
if s.Location != nil {
p.Set("location", s.Location.Location())
}
if s.Pano != "" {
p.Set("pano", s.Pano)
}
if s.Heading != nil {
p.Set("heading", fmt.Sprintf("%f", *s.Heading))
}
if s.FOV != nil {
p.Set("fov", fmt.Sprintf("%f", *s.FOV))
}
if s.Pitch != 0 {
p.Set("pitch", fmt.Sprintf("%f", s.Pitch))
}
}
package maps
import (
"fmt"
"net/url"
"time"
"golang.org/x/net/context"
)
// TimeZone requests time zone information about a location.
//
// See https://developers.google.com/maps/documentation/timezone/
func TimeZone(ctx context.Context, ll LatLng, t time.Time, opts *TimeZoneOpts) (*TimeZoneResult, error) {
var r timeZoneResponse
if err := doDecode(ctx, baseURL+timezone(ll, t, opts), &r); err != nil {
return nil, err
}
if r.Status != StatusOK {
return nil, APIError{r.Status, r.ErrorMessage}
}
return &r.TimeZoneResult, nil
}
func timezone(ll LatLng, t time.Time, opts *TimeZoneOpts) string {
p := url.Values{}
p.Set("location", ll.String())
p.Set("timestamp", fmt.Sprintf("%d", t.Unix()))
opts.update(p)
return "timezone/json?" + p.Encode()
}
// TimeZoneOpts defines options for TimeZone requests.
type TimeZoneOpts struct {
// The language in which to return results.
//
// See https://developers.google.com/maps/faq#languagesupport
Language string
}
func (t *TimeZoneOpts) update(p url.Values) {
if t == nil {
return
}
if t.Language != "" {
p.Set("language", t.Language)
}
}
type timeZoneResponse struct {
Status string `json:"status"`
ErrorMessage string `json:"error_message"`
TimeZoneResult
}
// TimeZoneResult describes information about the time zone at the requested location.
type TimeZoneResult struct {
// DSTOffset is the offset for daylight-savings time in seconds.
//
// This will be zero if the time zone is not in Daylight Savings Time during the specified time.
DSTOffset int64 `json:"dstOffset"`
// RawOffset is the offset from UTC (in seconds) for the given location.
//
// This does not take into effect daylight savings.
RawOffset int64 `json:"rawOffset"`
// TimeZoneID is a string containing the "tz" ID of the time zone, such as "America/Los_Angeles"
TimeZoneID string `json:"timeZoneId"`
// TimeZoneName is a string containing the long form name of the time zone, e.g., "Pacific Daylight Time".
//
// This field will be localized if the Language was specified.
TimeZoneName string `json:"timeZoneName"`
}
// DSTOffsetDuration translates the DSTOffset into a time.Duration
func (r TimeZoneResult) DSTOffsetDuration() time.Duration {
return time.Duration(r.DSTOffset) * time.Second
}
// RawOffsetDuration translates the RawOffset into a time.Duration
func (r TimeZoneResult) RawOffsetDuration() time.Duration {
return time.Duration(r.RawOffset) * time.Second
}
- Move code into /maps directory so clients can import ".../maps" instead of aliasing the import every time
- Add samples as subdirectories
- Flesh out README.md with usage information
- Add Example_whatever test cases that demonstrate usage
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment