diff --git a/directions.go b/directions.go index a1879bc..3e86789 100644 --- a/directions.go +++ b/directions.go @@ -33,7 +33,7 @@ const ( 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 { + if err := c.doDecode(baseURL+directions(orig, dest, opts), &d); err != nil { return nil, err } return &d, nil @@ -48,15 +48,11 @@ func directions(orig, dest Location, opts *DirectionsOpts) string { } type DirectionsOpts struct { - Mode string - Waypoints Route - Alternatives bool - Avoid []string - Language string - Units string - Region string - DepartureTime time.Time - ArrivalTime time.Time + Waypoints []Location + Alternatives bool + Avoid []string + Mode, Language, Units, Region string + DepartureTime, ArrivalTime time.Time } func (do *DirectionsOpts) update(p url.Values) { @@ -67,7 +63,7 @@ func (do *DirectionsOpts) update(p url.Values) { p.Set("mode", do.Mode) } if do.Waypoints != nil { - p.Set("waypoints", do.Waypoints.String()) + p.Set("waypoints", encodeLocations(do.Waypoints)) } if do.Alternatives { p.Set("alternatives", "true") diff --git a/maps.go b/maps.go index d777f48..1be838f 100644 --- a/maps.go +++ b/maps.go @@ -18,20 +18,22 @@ func NewClient(key string) Client { return Client{Key: key, Transport: http.DefaultTransport} } -func (c Client) do(url string, r interface{}) error { +func (c Client) do(url string) (*http.Response, error) { cl := &http.Client{Transport: c.Transport} req, err := http.NewRequest("GET", url, nil) if err != nil { - return err + return nil, err } - if c.Key != "" { q := req.URL.Query() q.Set("key", c.Key) req.URL.RawQuery = q.Encode() } + return cl.Get(url) +} - resp, err := cl.Get(url) +func (c Client) doDecode(url string, r interface{}) error { + resp, err := c.do(url) if err != nil { return err } @@ -76,11 +78,9 @@ 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 { +func encodeLocations(ls []Location) string { + s := make([]string, len(ls)) + for i, l := range ls { s[i] = l.Location() } return strings.Join(s, "|") diff --git a/maps_test.go b/maps_test.go index ec84f22..456d07b 100644 --- a/maps_test.go +++ b/maps_test.go @@ -9,13 +9,69 @@ func TestDirections(t *testing.T) { c := NewClient("") orig, dest := Address("111 8th Ave, NYC"), Address("170 E 92nd St, NYC") opts := &DirectionsOpts{ - Mode: ModeWalking, + Mode: ModeTransit, DepartureTime: time.Now(), + Alternatives: true, } t.Logf("%s", baseURL+directions(orig, dest, opts)) - r, err := c.Directions(orig, dest, nil) + r, err := c.Directions(orig, dest, opts) if err != nil { t.Errorf("unexpected error: %v", err) } t.Logf("%v", r) } + +func TestStaticMap(t *testing.T) { + c := NewClient("") + s := Size{512, 512} + opts := &StaticMapOpts{ + Markers: []Markers{ + { + Size: "small", + Color: "blue", + Locations: []Location{ + LatLng{1, 1}, + LatLng{2, 2}, + }, + }, { + Size: "mid", + Color: "red", + Locations: []Location{ + LatLng{3, 3}, + }, + }, + }, + Paths: []Path{ + { + Weight: 10, + Color: "green", + Locations: []Location{ + LatLng{4, 4}, + LatLng{5, 5}, + }, + }, { + Color: "0x00000000", + FillColor: "0x00000033", + 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: "0xFF0000"}, + }, + }, + }, + } + t.Logf("%s", baseURL+staticmap(s, opts)) + if _, err := c.StaticMap(s, opts); err != nil { + t.Errorf("unexpected error: %v", err) + } +} diff --git a/static.go b/static.go new file mode 100644 index 0000000..acab221 --- /dev/null +++ b/static.go @@ -0,0 +1,215 @@ +package maps + +import ( + "fmt" + "io" + "net/http" + "net/url" + "strings" +) + +const ( + MapFormatPNG = "png" + MapFormatPNG32 = "png32" + MapFormatGIF = "gif" + MapFormatJPG = "jpg" + MapFormatJPGBaseline = "jpg-baseline" + + MapTypeRoadmap = "roadmap" + MapTypeSatellite = "satellite" + MapTypeTerrain = "terrain" + MapTypeHybrid = "hybrid" +) + +func (c Client) StaticMap(s Size, opts *StaticMapOpts) (io.ReadCloser, error) { + resp, err := c.do(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() +} + +type Size struct { + H, W int +} + +func (s Size) String() string { + return fmt.Sprintf("%dx%d", s.H, s.W) +} + +type StaticMapOpts struct { + Center *Location + Zoom, Scale int + Format, MapType, Language, Region string + Markers []Markers + Paths []Path + Visible []Location + Styles []Style +} + +type Markers struct { + Size string // tiny, mid, small + Color string // hex color, or certain color names + Label string + IconURL string + HideShadow bool + Locations []Location +} + +func (m Markers) encode() string { + s := []string{} + if m.Size != "" { + s = append(s, "size:"+m.Size) + } + if m.Color != "" { + s = append(s, "color:"+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) +} + +type Path struct { + Weight int + Color string // with opacity, or certain color names + FillColor string // with opacity, or certain color names + Geodesic bool + Polyline string + 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 != "" { + s = append(s, "color:"+p.Color) + } + if p.FillColor != "" { + s = append(s, "fillcolor:"+p.FillColor) + } + if p.Geodesic { + s = append(s, "geodesic:true") + } + style := strings.Join(s, "|") + if style != "" { + style += "|" + } + if p.Polyline != "" { + return style + p.Polyline + } + return style + encodeLocations(p.Locations) +} + +type Style struct { + Feature string // TODO enum with sub-options + Element string // TODO enum + 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, "|") +} + +type StyleRule struct { + Hue string // rgb color + Lightness float64 // -100 to 100 + Saturation float64 // -100 to 100 + Gamma *float64 // .01 to 10, default 1 + InverseLightness *bool + Visibility string // on, off, simplified +} + +func (r StyleRule) encode() string { + s := []string{} + if r.Hue != "" { + s = append(s, "hue:"+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 != nil && *r.InverseLightness == false { + s = append(s, "inverse_lightness:false") + } + 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()) + } +}