├── README.md ├── geocode.go └── geocode_test.go /README.md: -------------------------------------------------------------------------------- 1 | # This package has moved 2 | 3 | Its new home is https://github.com/talmai/geocode 4 | 5 | Please update your import paths to `"github.com/talmai/geocode"` 6 | -------------------------------------------------------------------------------- /geocode.go: -------------------------------------------------------------------------------- 1 | // Package geocode has moved to "github.com/talmai/geocode". 2 | package geocode 3 | 4 | import ( 5 | "encoding/json" 6 | "fmt" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "strconv" 11 | ) 12 | 13 | const msg = ` 14 | *** 15 | Package geocode has moved to "github.com/talmai/geocode". 16 | The program's author should update it to use the package from its new location. 17 | *** 18 | ` 19 | 20 | func init() { 21 | fmt.Fprint(os.Stderr, msg) 22 | } 23 | 24 | type ( 25 | RequestType int 26 | ProviderApiLocation string 27 | ) 28 | 29 | const ( 30 | /* Request Type */ 31 | GEOCODE RequestType = 1 32 | ROUTE RequestType = 2 33 | 34 | /* Geocoding URLs */ 35 | GOOGLE = "http://maps.googleapis.com/maps/api/geocode/json" 36 | OSM = "http://open.mapquestapi.com/nominatim/v1/reverse.php" 37 | 38 | /* Routing URLs */ 39 | YOURS = "http://www.yournavigation.org/api/1.0/gosmore.php" 40 | YOURS_HEADER = "github.com/talmai/geocode" // change this to your website! 41 | ) 42 | 43 | type Point struct { 44 | Lat, Lng float64 45 | } 46 | 47 | func (p Point) String() string { 48 | return fmt.Sprintf("%g,%g", p.Lat, p.Lng) 49 | } 50 | 51 | type Bounds struct { 52 | NorthEast, SouthWest Point 53 | } 54 | 55 | func (b Bounds) String() string { 56 | return fmt.Sprintf("%v|%v", b.NorthEast, b.SouthWest) 57 | } 58 | 59 | type Request struct { 60 | HTTPClient *http.Client 61 | Provider ProviderApiLocation 62 | Type RequestType 63 | 64 | // For geocoding, one (and only one) of these must be set. 65 | Address string 66 | Location *Point 67 | 68 | // For routing, bounds must be set 69 | Bounds *Bounds // used by GOOGLE and YOURS 70 | 71 | Limit int64 // used by OSM 72 | Region string // used by GOOGLE 73 | Language string // used by GOOGLE 74 | Sensor bool // used by GOOGLE 75 | 76 | values url.Values 77 | } 78 | 79 | func (r *Request) Values() url.Values { 80 | if r.values == nil { 81 | r.values = make(url.Values) 82 | } 83 | var v = r.values 84 | 85 | if r.Address != "" { 86 | switch r.Provider { 87 | case GOOGLE: 88 | v.Set("address", r.Address) 89 | case OSM: 90 | v.Set("q", r.Address) 91 | } 92 | } else if r.Location != nil { 93 | switch r.Provider { 94 | case GOOGLE: 95 | v.Set("latlng", r.Location.String()) 96 | case OSM: 97 | v.Set("lat", fmt.Sprintf("%g", r.Location.Lat)) 98 | v.Set("lon", fmt.Sprintf("%g", r.Location.Lng)) 99 | } 100 | } else { 101 | if r.Type == GEOCODE { 102 | panic("neither Address nor Location set") 103 | } 104 | } 105 | 106 | if r.Bounds != nil { 107 | switch r.Provider { 108 | case GOOGLE: 109 | v.Set("bounds", r.Bounds.String()) 110 | case YOURS: 111 | v.Set("flat", fmt.Sprintf("%g", r.Bounds.NorthEast.Lat)) 112 | v.Set("flon", fmt.Sprintf("%g", r.Bounds.NorthEast.Lng)) 113 | v.Set("tlat", fmt.Sprintf("%g", r.Bounds.SouthWest.Lat)) 114 | v.Set("tlon", fmt.Sprintf("%g", r.Bounds.SouthWest.Lng)) 115 | } 116 | } else { 117 | if r.Type == ROUTE { 118 | panic("Start/End Bounds must be set for routing") 119 | } 120 | } 121 | 122 | switch r.Provider { 123 | case GOOGLE: 124 | if r.Region != "" { 125 | v.Set("region", r.Region) 126 | } 127 | if r.Language != "" { 128 | v.Set("language", r.Language) 129 | } 130 | v.Set("sensor", strconv.FormatBool(r.Sensor)) 131 | case OSM: 132 | v.Set("limit", strconv.FormatInt(r.Limit, 10)) 133 | v.Set("format", "json") 134 | case YOURS: 135 | v.Set("v", "motorcar") // type of transport, possible options are: motorcar, bicycle or foot. 136 | v.Set("fast", "1") // selects the fastest route, 0 the shortest route. 137 | v.Set("layer", "mapnik") // determines which Gosmore instance is used to calculate the route 138 | // Provide mapnik for normal routing using car, bicycle or foot 139 | // Provide cn for using bicycle routing using cycle route networks only. 140 | v.Set("format", "geojson") // This can either be kml or geojson. 141 | v.Set("geometry", "1") // enables/disables adding the route geometry in the output. 142 | v.Set("distance", "v") // specifies which algorithm is used to calculate the route distance 143 | // Options are v for Vicenty, gc for simplified Great Circle, h for Haversine Law, cs for Cosine Law. 144 | v.Set("instructions", "1") // enbles/disables adding driving instructions in the output. 145 | v.Set("lang", "en_US") // specifies the language code in which the routing directions are returned. 146 | } 147 | 148 | return v 149 | } 150 | 151 | func (r *Request) Lookup(transport http.RoundTripper) (*Response, error) { 152 | r.Type = GEOCODE 153 | return r.SendAPIRequest(transport) 154 | } 155 | 156 | func (r *Request) Route(transport http.RoundTripper) (*Response, error) { 157 | r.Type = ROUTE 158 | return r.SendAPIRequest(transport) 159 | } 160 | 161 | // SendAPIRequest makes the Request to the provider using 162 | // the provided transport (or http.DefaultTransport if nil). 163 | func (r *Request) SendAPIRequest(transport http.RoundTripper) (*Response, error) { 164 | if r == nil { 165 | panic("Lookup on nil *Request") 166 | } 167 | 168 | c := r.HTTPClient 169 | if c == nil { 170 | c = &http.Client{Transport: transport} 171 | } 172 | u := fmt.Sprintf("%s?%s", r.Provider, r.Values().Encode()) 173 | 174 | req, err := http.NewRequest("GET", u, nil) 175 | if r.Provider == YOURS { 176 | req.Header.Add("X-Yours-client", YOURS_HEADER) 177 | } 178 | getResp, err := c.Do(req) 179 | if err != nil { 180 | return nil, err 181 | } 182 | defer getResp.Body.Close() 183 | 184 | resp := new(Response) 185 | resp.QueryString = u 186 | 187 | if getResp.StatusCode == 200 { // OK 188 | err = json.NewDecoder(getResp.Body).Decode(resp) 189 | switch r.Provider { 190 | case GOOGLE: 191 | // reverse geocoding 192 | resp.Count = len(resp.GoogleResponse.Results) 193 | if resp.Count >= 1 { 194 | resp.Found = resp.GoogleResponse.Results[0].Address 195 | } 196 | case OSM: 197 | // reverse geocoding 198 | if resp.OSMResponse.Address != "" { 199 | resp.Count = 1 200 | resp.Found = resp.OSMResponse.AddressParts.Name 201 | } else { 202 | resp.Count = 0 203 | } 204 | // geocoding 205 | // bodyBytes, _ := ioutil.ReadAll(getResp.Body) 206 | // bodyString := string(bodyBytes) 207 | // err = json.NewDecoder(bytes.NewBufferString(strings.TrimRight(bodyString, "]")[1:])).Decode(resp) 208 | } 209 | } 210 | 211 | if err != nil { 212 | return nil, err 213 | } 214 | 215 | resp.Status = "OK" 216 | return resp, nil 217 | } 218 | 219 | type Response struct { 220 | Status string 221 | QueryString string 222 | Found string 223 | Count int 224 | *GoogleResponse 225 | *OSMResponse 226 | *YOURSResponse 227 | } 228 | 229 | type GoogleResponse struct { 230 | Results []*GoogleResult 231 | } 232 | 233 | type GoogleResult struct { 234 | /* 235 | { 236 | "results" : [ 237 | { 238 | "address_components" : [ 239 | { 240 | "long_name" : "1600", 241 | "short_name" : "1600", 242 | "types" : [ "street_number" ] 243 | }, 244 | { 245 | "long_name" : "Amphitheatre Pkwy", 246 | "short_name" : "Amphitheatre Pkwy", 247 | "types" : [ "route" ] 248 | }, 249 | { 250 | "long_name" : "Mountain View", 251 | "short_name" : "Mountain View", 252 | "types" : [ "locality", "political" ] 253 | }, 254 | { 255 | "long_name" : "Santa Clara", 256 | "short_name" : "Santa Clara", 257 | "types" : [ "administrative_area_level_2", "political" ] 258 | }, 259 | { 260 | "long_name" : "California", 261 | "short_name" : "CA", 262 | "types" : [ "administrative_area_level_1", "political" ] 263 | }, 264 | { 265 | "long_name" : "United States", 266 | "short_name" : "US", 267 | "types" : [ "country", "political" ] 268 | }, 269 | { 270 | "long_name" : "94043", 271 | "short_name" : "94043", 272 | "types" : [ "postal_code" ] 273 | } 274 | ], 275 | "formatted_address" : "1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA", 276 | "geometry" : { 277 | "location" : { 278 | "lat" : 37.42291810, 279 | "lng" : -122.08542120 280 | }, 281 | "location_type" : "ROOFTOP", 282 | "viewport" : { 283 | "northeast" : { 284 | "lat" : 37.42426708029149, 285 | "lng" : -122.0840722197085 286 | }, 287 | "southwest" : { 288 | "lat" : 37.42156911970850, 289 | "lng" : -122.0867701802915 290 | } 291 | } 292 | }, 293 | "types" : [ "street_address" ] 294 | } 295 | ], 296 | "status" : "OK" 297 | } 298 | */ 299 | Address string `json:"formatted_address"` 300 | AddressParts []*GoogleAddressPart `json:"address_components"` 301 | Geometry *Geometry 302 | Types []string 303 | } 304 | 305 | type GoogleAddressPart struct { 306 | Name string `json:"long_name"` 307 | ShortName string `json:"short_name"` 308 | Types []string 309 | } 310 | 311 | type Geometry struct { 312 | Bounds Bounds 313 | Location Point 314 | Type string 315 | Viewport Bounds 316 | } 317 | 318 | type OSMResponse struct { 319 | // OSM stuff 320 | /* 321 | {"place_id":"62762024", 322 | "licence":"Data \u00a9 OpenStreetMap contributors, ODbL 1.0. http:\/\/www.openstreetmap.org\/copyright", 323 | "osm_type":"way", 324 | "osm_id":"90394420", 325 | "lat":"52.548781", 326 | "lon":"-1.81626870827795", 327 | "display_name":"137, Pilkington Avenue, Castle Vale, Birmingham, West Midlands, England, B72 1LH, United Kingdom", 328 | "address":{ 329 | "house_number":"137", 330 | "road":"Pilkington Avenue", 331 | "suburb":"Castle Vale", 332 | "city":"Birmingham", 333 | "county":"West Midlands", 334 | "state_district":"West Midlands", 335 | "state":"England", 336 | "postcode":"B72 1LH", 337 | "country":"United Kingdom", 338 | "country_code":"gb" 339 | }} 340 | */ 341 | Address string `json:"display_name"` 342 | AddressParts *OSMAddressPart `json:"address"` 343 | Lat string `json:"lat"` 344 | Lng string `json:"lon"` 345 | } 346 | 347 | type OSMAddressPart struct { 348 | HouseNumber string `json:"house_number"` 349 | Name string `json:"road"` 350 | City string `json:"city"` 351 | State string `json:"state"` 352 | } 353 | 354 | // currently doesn't 355 | type YOURSResponse struct { 356 | /* 357 | { 358 | "type": "LineString", 359 | "crs": { 360 | "type": "name", 361 | "properties": { 362 | "name": "urn:ogc:def:crs:OGC:1.3:CRS84" 363 | } 364 | }, 365 | "coordinates": 366 | [ 367 | [-118.604871, 34.172300] 368 | ,[-118.604872, 34.172078] 369 | ,[-118.604870, 34.171966] 370 | ,[-118.500806, 34.235753] 371 | ,[-118.500814, 34.236146] 372 | ], 373 | "properties": { 374 | "distance": "17.970238", 375 | "description": "Go straight ahead.
Follow the road for...", 376 | "traveltime": "1018" 377 | } 378 | } 379 | */ 380 | Coordinates [][]float64 `json:"coordinates"` 381 | Properties *YOURSProperties `json:"properties"` 382 | } 383 | 384 | type YOURSProperties struct { 385 | Distance string `json:"distance"` 386 | Instructions string `json:"description"` 387 | TravelTime string `json:"traveltime"` 388 | } 389 | -------------------------------------------------------------------------------- /geocode_test.go: -------------------------------------------------------------------------------- 1 | package geocode 2 | 3 | import "testing" 4 | 5 | func TestMoved(t *testing.T) { 6 | t.Log(msg) 7 | t.Fail() 8 | } 9 | 10 | func TestLookup(t *testing.T) { 11 | req := &Request{ 12 | Address: "New York City", 13 | Provider: GOOGLE, 14 | } 15 | resp, err := req.Lookup(nil) 16 | if err != nil { 17 | t.Fatalf("Lookup error: %v", err) 18 | } 19 | if s := resp.Status; s != "OK" { 20 | t.Fatalf(`Status == %q, want "OK"`, s) 21 | } 22 | if l := len(resp.Results); l != 1 { 23 | t.Fatalf("len(Results) == %d, want 1", l) 24 | } 25 | addr := "New York, NY, USA" 26 | if a := resp.Found; a != addr { 27 | t.Errorf("Address == %q, want %q", a, addr) 28 | } 29 | } 30 | 31 | func TestLookupWithBounds(t *testing.T) { 32 | req := &Request{ 33 | Address: "Winnetka", 34 | Provider: GOOGLE, 35 | } 36 | bounds := &Bounds{Point{34.172684, -118.604794}, 37 | Point{34.236144, -118.500938}} 38 | req.Bounds = bounds 39 | resp, err := req.Lookup(nil) 40 | if err != nil { 41 | t.Fatalf("Lookup error: %v", err) 42 | } 43 | if s := resp.Status; s != "OK" { 44 | t.Fatalf(`Status == %q, want "OK"`, s) 45 | } 46 | if l := len(resp.Results); l != 1 { 47 | t.Fatalf("len(Results) == %d, want 1", l) 48 | } 49 | addr := "Winnetka, Los Angeles, CA, USA" 50 | if a := resp.Found; a != addr { 51 | t.Errorf("Address == %q, want %q", a, addr) 52 | } 53 | } 54 | 55 | func TestLookupWithLanguage(t *testing.T) { 56 | req := &Request{ 57 | Address: "札幌市", 58 | Provider: GOOGLE, 59 | } 60 | req.Language = "ja" 61 | resp, err := req.Lookup(nil) 62 | if err != nil { 63 | t.Fatalf("Lookup error: %v", err) 64 | } 65 | if s := resp.Status; s != "OK" { 66 | t.Fatalf(`Status == %q, want "OK"`, s) 67 | } 68 | if l := len(resp.Results); l != 1 { 69 | t.Fatalf("len(Results) == %d, want 1", l) 70 | } 71 | addr := "日本, 北海道札幌市" 72 | if a := resp.Found; a != addr { 73 | t.Errorf("Address == %q, want %q", a, addr) 74 | } 75 | } 76 | 77 | func TestLookupWithRegion(t *testing.T) { 78 | req := &Request{ 79 | Address: "Toledo", 80 | Provider: GOOGLE, 81 | } 82 | req.Region = "es" 83 | resp, err := req.Lookup(nil) 84 | if err != nil { 85 | t.Fatalf("Lookup error: %v", err) 86 | } 87 | if s := resp.Status; s != "OK" { 88 | t.Fatalf(`Status == %q, want "OK"`, s) 89 | } 90 | if l := len(resp.Results); l != 1 { 91 | t.Fatalf("len(Results) == %d, want 1", l) 92 | } 93 | addr := "Toledo, Spain" 94 | if a := resp.Found; a != addr { 95 | t.Errorf("Address == %q, want %q", a, addr) 96 | } 97 | } 98 | --------------------------------------------------------------------------------