diff --git a/.travis.yml b/.travis.yml index 0a51e89..9ee4571 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: go sudo: false go: -- '1.5' +- '1.7' - tip go_import_path: github.com/codingsince1985/geo-golang before_install: diff --git a/README.md b/README.md index a0ea6cb..f4cb29a 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ GeoService in Go == -[![GoDoc](https://godoc.org/github.com/codingsince1985/geo-golang?status.svg)](https://godoc.org/github.com/codingsince1985/geo-golang) [![Build Status](https://travis-ci.org/codingsince1985/geo-golang.svg?branch=master)](https://travis-ci.org/codingsince1985/geo-golang) +[![GoDoc](https://godoc.org/github.com/codingsince1985/geo-golang?status.svg)](https://godoc.org/github.com/codingsince1985/geo-golang) [![Build Status](https://travis-ci.org/codingsince1985/geo-golang.svg?branch=master)](https://travis-ci.org/codingsince1985/geo-golang) [![codecov](https://codecov.io/gh/codingsince1985/geo-golang/branch/master/graph/badge.svg)](https://codecov.io/gh/codingsince1985/geo-golang) [![Go Report Card](https://goreportcard.com/badge/codingsince1985/geo-golang)](https://goreportcard.com/report/codingsince1985/geo-golang) @@ -51,11 +51,15 @@ const ( ) func main() { + ExampleGeocoder() +} + +func ExampleGeocoder() { fmt.Println("Google Geocoding API") try(google.Geocoder(os.Getenv("GOOGLE_API_KEY"))) fmt.Println("Mapquest Nominatim") - try(nominatim.Geocoder(os.Getenv("MAPQUEST_NOMINATUM_KEY"))) + try(nominatim.Geocoder(os.Getenv("MAPQUEST_NOMINATIM_KEY"))) fmt.Println("Mapquest Open streetmaps") try(open.Geocoder(os.Getenv("MAPQUEST_OPEN_KEY"))) @@ -74,7 +78,7 @@ func main() { fmt.Println("OpenStreetMap") try(openstreetmap.Geocoder()) - + fmt.Println("LocationIQ") try(locationiq.Geocoder(os.Getenv("LOCATIONIQ_API_KEY"), ZOOM)) @@ -88,52 +92,72 @@ func main() { func try(geocoder geo.Geocoder) { location, _ := geocoder.Geocode(addr) - fmt.Printf("%s location is %v\n", addr, location) + if location != nil { + fmt.Printf("%s location is (%.6f, %.6f)\n", addr, location.Lat, location.Lng) + } else { + fmt.Println("got location") + } address, _ := geocoder.ReverseGeocode(lat, lng) - fmt.Printf("Address of (%f,%f) is %s\n\n", lat, lng, address) + if address != nil { + fmt.Printf("Address of (%.6f,%.6f) is %s\n", lat, lng, address.FormattedAddress) + fmt.Printf("Detailed address: %#v\n", address) + } else { + fmt.Println("got address") + } + fmt.Println("\n") } ``` ###Result ``` Google Geocoding API Melbourne VIC location is (-37.813611, 144.963056) -Address of (-37.813611,144.963056) is 350 Bourke St, Melbourne VIC 3004, Australia +Address of (-37.813611,144.963056) is 197 Elizabeth St, Melbourne VIC 3000, Australia +Detailed address: &geo.Address{FormattedAddress:"197 Elizabeth St, Melbourne VIC 3000, Australia", Street:"Elizabeth Street", HouseNumber:"197", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"Melbourne City", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} Mapquest Nominatim Melbourne VIC location is (-37.814218, 144.963161) -Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia +Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia +Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"City of Melbourne", Country:"Australia", CountryCode:"AU", City:"Melbourne"} Mapquest Open streetmaps Melbourne VIC location is (-37.814218, 144.963161) -Address of (-37.813611,144.963056) is Postal Lane, Melbourne, Victoria, AU +Address of (-37.813611,144.963056) is Elizabeth Street, 3000, Melbourne, Victoria, AU +Detailed address: &geo.Address{FormattedAddress:"Elizabeth Street, 3000, Melbourne, Victoria, AU", Street:"Elizabeth Street", HouseNumber:"", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"", CountryCode:"AU", City:"Melbourne"} OpenCage Data Melbourne VIC location is (-37.814217, 144.963161) Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Melbourne VIC 3000, Australia +Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Melbourne VIC 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne (3000)", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"City of Melbourne", Country:"Australia", CountryCode:"AU", City:"Melbourne"} HERE API Melbourne VIC location is (-37.817530, 144.967150) Address of (-37.813611,144.963056) is 197 Elizabeth St, Melbourne VIC 3000, Australia +Detailed address: &geo.Address{FormattedAddress:"197 Elizabeth St, Melbourne VIC 3000, Australia", Street:"Elizabeth St", HouseNumber:"197", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AUS", City:"Melbourne"} Bing Geocoding API Melbourne VIC location is (-37.824299, 144.977997) Address of (-37.813611,144.963056) is Elizabeth St, Melbourne, VIC 3000 +Detailed address: &geo.Address{FormattedAddress:"Elizabeth St, Melbourne, VIC 3000", Street:"Elizabeth St", HouseNumber:"", Suburb:"", Postcode:"3000", State:"", StateDistrict:"", County:"", Country:"Australia", CountryCode:"", City:"Melbourne"} Mapbox API Melbourne VIC location is (-37.814200, 144.963200) -Address of (-37.813611,144.963056) is Elwood Park Playground, 3000 Melbourne, Australia +Address of (-37.813611,144.963056) is Elwood Park Playground, Melbourne, Victoria 3000, Australia +Detailed address: &geo.Address{FormattedAddress:"Elwood Park Playground, Melbourne, Victoria 3000, Australia", Street:"Elwood Park Playground", HouseNumber:"", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} OpenStreetMap Melbourne VIC location is (-37.814217, 144.963161) Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia +Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} LocationIQ Melbourne VIC location is (-37.814217, 144.963161) Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia +Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} ChainedAPI[OpenStreetmap -> Google] Melbourne VIC location is (-37.814217, 144.963161) Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia +Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} ``` License == diff --git a/bing/geocoder.go b/bing/geocoder.go index 3037c9b..2676398 100644 --- a/bing/geocoder.go +++ b/bing/geocoder.go @@ -2,9 +2,11 @@ package bing import ( + "errors" "fmt" - "github.com/codingsince1985/geo-golang" "strings" + + "github.com/codingsince1985/geo-golang" ) type ( @@ -17,9 +19,16 @@ type ( } Address struct { FormattedAddress string + AddressLine string + AdminDistrict string + AdminDistrict2 string + CountryRegion string + Locality string + PostalCode string } } } + ErrorDetails []string } ) @@ -46,17 +55,31 @@ func (b baseURL) ReverseGeocodeURL(l geo.Location) string { return strings.Replace(string(b), "*", fmt.Sprintf("/%f,%f?", l.Lat, l.Lng), 1) } -func (r *geocodeResponse) Location() geo.Location { - if len(r.ResourceSets) == 0 || len(r.ResourceSets[0].Resources) == 0 { - return geo.Location{} +func (r *geocodeResponse) Location() (*geo.Location, error) { + if len(r.ResourceSets) <= 0 || len(r.ResourceSets[0].Resources) <= 0 { + return nil, nil } c := r.ResourceSets[0].Resources[0].Point.Coordinates - return geo.Location{c[0], c[1]} + return &geo.Location{ + Lat: c[0], + Lng: c[1], + }, nil } -func (r *geocodeResponse) Address() string { - if len(r.ResourceSets) == 0 || len(r.ResourceSets[0].Resources) == 0 { - return "" +func (r *geocodeResponse) Address() (*geo.Address, error) { + if len(r.ErrorDetails) > 0 { + return nil, errors.New(strings.Join(r.ErrorDetails, " ")) } - return r.ResourceSets[0].Resources[0].Address.FormattedAddress + if len(r.ResourceSets) <= 0 || len(r.ResourceSets[0].Resources) <= 0 { + return nil, nil + } + + a := r.ResourceSets[0].Resources[0].Address + return &geo.Address{ + FormattedAddress: a.FormattedAddress, + Street: a.AddressLine, + City: a.Locality, + Postcode: a.PostalCode, + Country: a.CountryRegion, + }, nil } diff --git a/bing/geocoder_test.go b/bing/geocoder_test.go index 119ec34..de081c7 100644 --- a/bing/geocoder_test.go +++ b/bing/geocoder_test.go @@ -1,15 +1,15 @@ package bing_test import ( - "fmt" - "github.com/codingsince1985/geo-golang" - "github.com/codingsince1985/geo-golang/bing" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "os" "strings" "testing" + + "github.com/codingsince1985/geo-golang" + "github.com/codingsince1985/geo-golang/bing" + "github.com/stretchr/testify/assert" ) var key = os.Getenv("BING_API_KEY") @@ -20,9 +20,9 @@ func TestGeocode(t *testing.T) { geocoder := bing.Geocoder(key, ts.URL+"/") location, err := geocoder.Geocode("60 Collins St, Melbourne VIC") + assert.NoError(t, err) - fmt.Println(location) - assert.Equal(t, geo.Location{Lat: -37.81375, Lng: 144.97176}, location) + assert.Equal(t, geo.Location{Lat: -37.81375, Lng: 144.97176}, *location) } func TestReverseGeocode(t *testing.T) { @@ -31,9 +31,9 @@ func TestReverseGeocode(t *testing.T) { geocoder := bing.Geocoder(key, ts.URL+"/") address, err := geocoder.ReverseGeocode(-37.81375, 144.97176) + assert.NoError(t, err) - fmt.Println(address) - assert.True(t, strings.Index(address, "Collins St") > 0) + assert.True(t, strings.Index(address.FormattedAddress, "Collins St") > 0) } func TestReverseGeocodeWithNoResult(t *testing.T) { @@ -41,8 +41,10 @@ func TestReverseGeocodeWithNoResult(t *testing.T) { defer ts.Close() geocoder := bing.Geocoder(key, ts.URL+"/") - _, err := geocoder.ReverseGeocode(-37.81375, 164.97176) - assert.Equal(t, err, geo.ErrNoResult) + addr, err := geocoder.ReverseGeocode(-37.81375, 164.97176) + + assert.NoError(t, err) + assert.Nil(t, addr) } func testServer(response string) *httptest.Server { diff --git a/cached/geocoder.go b/cached/geocoder.go index 2559625..6309e57 100644 --- a/cached/geocoder.go +++ b/cached/geocoder.go @@ -18,10 +18,10 @@ func Geocoder(geocoder geo.Geocoder, cache *cache.Cache) geo.Geocoder { } // Geocode returns location for address -func (c cachedGeocoder) Geocode(address string) (geo.Location, error) { +func (c cachedGeocoder) Geocode(address string) (*geo.Location, error) { // Check if we've cached this response if cachedLoc, found := c.Cache.Get(address); found { - return cachedLoc.(geo.Location), nil + return cachedLoc.(*geo.Location), nil } if loc, err := c.Geocoder.Geocode(address); err != nil { @@ -33,15 +33,15 @@ func (c cachedGeocoder) Geocode(address string) (geo.Location, error) { } // ReverseGeocode returns address for location -func (c cachedGeocoder) ReverseGeocode(lat, lng float64) (string, error) { +func (c cachedGeocoder) ReverseGeocode(lat, lng float64) (*geo.Address, error) { // Check if we've cached this response locKey := fmt.Sprintf("geo.Location{%f,%f}", lat, lng) if cachedAddr, found := c.Cache.Get(locKey); found { - return cachedAddr.(string), nil + return cachedAddr.(*geo.Address), nil } if addr, err := c.Geocoder.ReverseGeocode(lat, lng); err != nil { - return "", err + return nil, err } else { c.Cache.Set(locKey, addr, 0) return addr, nil diff --git a/cached/geocoder_test.go b/cached/geocoder_test.go index a6343e3..b5e5118 100644 --- a/cached/geocoder_test.go +++ b/cached/geocoder_test.go @@ -1,15 +1,15 @@ package cached_test import ( + "strings" + "testing" + "time" + "github.com/codingsince1985/geo-golang" "github.com/codingsince1985/geo-golang/cached" "github.com/codingsince1985/geo-golang/data" "github.com/patrickmn/go-cache" "github.com/stretchr/testify/assert" - - "strings" - "testing" - "time" ) var geoCache = cache.New(5*time.Minute, 30*time.Second) @@ -17,55 +17,69 @@ var geoCache = cache.New(5*time.Minute, 30*time.Second) // geocoder is chained with one data geocoder with address -> location data // the other has location -> address data // this will exercise the chained fallback handling -var geocoder = cached.Geocoder( - data.Geocoder( - data.AddressToLocation{ - "Melbourne VIC": geo.Location{Lat: -37.814107, Lng: 144.96328}, - }, - data.LocationToAddress{ - geo.Location{Lat: -37.816742, Lng: 144.964463}: "Melbourne VIC 3000, Australia", - }, - ), - geoCache, +var ( + addressFixture = geo.Address{ + FormattedAddress: "64 Elizabeth Street, Melbourne, Victoria 3000, Australia", + } + locationFixture = geo.Location{ + Lat: -37.814107, + Lng: 144.96328, + } + geocoder = cached.Geocoder( + data.Geocoder( + data.AddressToLocation{ + addressFixture: locationFixture, + }, + data.LocationToAddress{ + locationFixture: addressFixture, + }, + ), + geoCache, + ) ) func TestGeocode(t *testing.T) { - location, err := geocoder.Geocode("Melbourne VIC") + location, err := geocoder.Geocode("64 Elizabeth Street, Melbourne, Victoria 3000, Australia") assert.NoError(t, err) - assert.Equal(t, geo.Location{Lat: -37.814107, Lng: 144.96328}, location) + assert.Equal(t, locationFixture, *location) } func TestReverseGeocode(t *testing.T) { - address, err := geocoder.ReverseGeocode(-37.816742, 144.964463) + address, err := geocoder.ReverseGeocode(locationFixture.Lat, locationFixture.Lng) assert.NoError(t, err) - assert.True(t, strings.HasSuffix(address, "Melbourne VIC 3000, Australia")) + assert.True(t, strings.HasSuffix(address.FormattedAddress, "Melbourne, Victoria 3000, Australia")) } func TestReverseGeocodeWithNoResult(t *testing.T) { - _, err := geocoder.ReverseGeocode(-37.816742, 164.964463) - assert.Equal(t, err, geo.ErrNoResult) + addr, err := geocoder.ReverseGeocode(1, 2) + assert.Nil(t, err) + assert.Nil(t, addr) } func TestCachedGeocode(t *testing.T) { + mockAddr := geo.Address{ + FormattedAddress: "42, Some Street, Austin, Texas", + } mock1 := data.Geocoder( data.AddressToLocation{ - "Austin,TX": geo.Location{Lat: 1, Lng: 2}, + mockAddr: geo.Location{Lat: 1, Lng: 2}, }, data.LocationToAddress{}, ) c := cached.Geocoder(mock1, geoCache) - l, err := c.Geocode("Austin,TX") + l, err := c.Geocode("42, Some Street, Austin, Texas") assert.NoError(t, err) - assert.Equal(t, geo.Location{Lat: 1, Lng: 2}, l) + assert.Equal(t, geo.Location{Lat: 1, Lng: 2}, *l) // Should be cached // TODO: write a mock Cache impl to test cache is being used - l, err = c.Geocode("Austin,TX") + l, err = c.Geocode("42, Some Street, Austin, Texas") assert.NoError(t, err) - assert.Equal(t, geo.Location{Lat: 1, Lng: 2}, l) + assert.Equal(t, geo.Location{Lat: 1, Lng: 2}, *l) - _, err = c.Geocode("NOWHERE,TX") - assert.Equal(t, geo.ErrNoResult, err) + addr, err := c.Geocode("NOWHERE,TX") + assert.Nil(t, err) + assert.Nil(t, addr) } diff --git a/chained/geocoder.go b/chained/geocoder.go index abb2419..35e14a6 100644 --- a/chained/geocoder.go +++ b/chained/geocoder.go @@ -10,29 +10,29 @@ type chainedGeocoder struct{ Geocoders []geo.Geocoder } func Geocoder(geocoders ...geo.Geocoder) geo.Geocoder { return chainedGeocoder{Geocoders: geocoders} } // Geocode returns location for address -func (c chainedGeocoder) Geocode(address string) (geo.Location, error) { +func (c chainedGeocoder) Geocode(address string) (*geo.Location, error) { // Geocode address by each geocoder until we get a real location response for i := range c.Geocoders { - if l, err := c.Geocoders[i].Geocode(address); err == nil { + if l, err := c.Geocoders[i].Geocode(address); err == nil && l != nil { return l, nil } // skip error and try the next geocoder continue } // No geocoders found a result - return geo.Location{}, geo.ErrNoResult + return nil, nil } // ReverseGeocode returns address for location -func (c chainedGeocoder) ReverseGeocode(lat, lng float64) (string, error) { +func (c chainedGeocoder) ReverseGeocode(lat, lng float64) (*geo.Address, error) { // Geocode address by each geocoder until we get a real location response for i := range c.Geocoders { - if addr, err := c.Geocoders[i].ReverseGeocode(lat, lng); err == nil { + if addr, err := c.Geocoders[i].ReverseGeocode(lat, lng); err == nil && addr != nil { return addr, nil } // skip error and try the next geocoder continue } // No geocoders found a result - return "", geo.ErrNoResult + return nil, nil } diff --git a/chained/geocoder_test.go b/chained/geocoder_test.go index 606f87d..4726513 100644 --- a/chained/geocoder_test.go +++ b/chained/geocoder_test.go @@ -1,62 +1,72 @@ package chained_test import ( + "strings" + "testing" + "github.com/codingsince1985/geo-golang" "github.com/codingsince1985/geo-golang/chained" "github.com/codingsince1985/geo-golang/data" "github.com/stretchr/testify/assert" - - "strings" - "testing" ) // geocoder is chained with one data geocoder with address -> location data // the other has location -> address data // this will exercise the chained fallback handling -var geocoder = chained.Geocoder( - data.Geocoder( - data.AddressToLocation{ - "Melbourne VIC": geo.Location{Lat: -37.814107, Lng: 144.96328}, - }, - data.LocationToAddress{}, - ), +var ( + addressFixture = geo.Address{ + FormattedAddress: "64 Elizabeth Street, Melbourne, Victoria 3000, Australia", + } + locationFixture = geo.Location{ + Lat: -37.814107, + Lng: 144.96328, + } + geocoder = chained.Geocoder( + data.Geocoder( + data.AddressToLocation{ + addressFixture: locationFixture, + }, + data.LocationToAddress{}, + ), - data.Geocoder( - data.AddressToLocation{}, - data.LocationToAddress{ - geo.Location{Lat: -37.816742, Lng: 144.964463}: "Melbourne VIC 3000, Australia", - }, - ), + data.Geocoder( + data.AddressToLocation{}, + data.LocationToAddress{ + locationFixture: addressFixture, + }, + ), + ) ) func TestGeocode(t *testing.T) { - location, err := geocoder.Geocode("Melbourne VIC") + location, err := geocoder.Geocode(addressFixture.FormattedAddress) assert.NoError(t, err) - assert.Equal(t, geo.Location{Lat: -37.814107, Lng: 144.96328}, location) + assert.Equal(t, geo.Location{locationFixture.Lat, locationFixture.Lng}, *location) } func TestReverseGeocode(t *testing.T) { - address, err := geocoder.ReverseGeocode(-37.816742, 144.964463) + address, err := geocoder.ReverseGeocode(locationFixture.Lat, locationFixture.Lng) assert.NoError(t, err) - assert.True(t, strings.HasSuffix(address, "Melbourne VIC 3000, Australia")) + assert.True(t, strings.HasSuffix(address.FormattedAddress, "Melbourne, Victoria 3000, Australia")) } func TestReverseGeocodeWithNoResult(t *testing.T) { - _, err := geocoder.ReverseGeocode(-37.816742, 164.964463) - assert.Equal(t, err, geo.ErrNoResult) + addr, err := geocoder.ReverseGeocode(0, 0) + assert.Nil(t, err) + assert.Nil(t, addr) } func TestChainedGeocode(t *testing.T) { mock1 := data.Geocoder( data.AddressToLocation{ - "Austin,TX": geo.Location{Lat: 1, Lng: 2}, + geo.Address{FormattedAddress: "Austin,TX"}: geo.Location{Lat: 1, Lng: 2}, }, data.LocationToAddress{}, ) mock2 := data.Geocoder( data.AddressToLocation{ - "Dallas,TX": geo.Location{Lat: 3, Lng: 4}, + geo.Address{FormattedAddress: "Dallas,TX"}: geo.Location{Lat: 3, Lng: 4}, }, data.LocationToAddress{}, ) @@ -65,12 +75,13 @@ func TestChainedGeocode(t *testing.T) { l, err := c.Geocode("Austin,TX") assert.NoError(t, err) - assert.Equal(t, geo.Location{Lat: 1, Lng: 2}, l) + assert.Equal(t, geo.Location{Lat: 1, Lng: 2}, *l) l, err = c.Geocode("Dallas,TX") assert.NoError(t, err) - assert.Equal(t, geo.Location{Lat: 3, Lng: 4}, l) + assert.Equal(t, geo.Location{Lat: 3, Lng: 4}, *l) - _, err = c.Geocode("NOWHERE,TX") - assert.Equal(t, geo.ErrNoResult, err) + addr, err := c.Geocode("NOWHERE,TX") + assert.Nil(t, err) + assert.Nil(t, addr) } diff --git a/data/geocoder.go b/data/geocoder.go index 95f3c2a..0e398e7 100644 --- a/data/geocoder.go +++ b/data/geocoder.go @@ -5,10 +5,10 @@ import ( ) // AddressToLocation maps address string to location (lat, long) -type AddressToLocation map[string]geo.Location +type AddressToLocation map[geo.Address]geo.Location // LocationToAddress maps location(lat,lng) to address -type LocationToAddress map[geo.Location]string +type LocationToAddress map[geo.Location]geo.Address // dataGeocoder represents geo data in memory type dataGeocoder struct { @@ -25,17 +25,21 @@ func Geocoder(addressToLocation AddressToLocation, LocationToAddress LocationToA } // Geocode returns location for address -func (d dataGeocoder) Geocode(address string) (geo.Location, error) { - if l, ok := d.AddressToLocation[address]; ok { - return l, nil +func (d dataGeocoder) Geocode(address string) (*geo.Location, error) { + addr := geo.Address{ + FormattedAddress: address, } - return geo.Location{}, geo.ErrNoResult + if l, ok := d.AddressToLocation[addr]; ok { + return &l, nil + } + + return nil, nil } // ReverseGeocode returns address for location -func (d dataGeocoder) ReverseGeocode(lat, lng float64) (string, error) { +func (d dataGeocoder) ReverseGeocode(lat, lng float64) (*geo.Address, error) { if address, ok := d.LocationToAddress[geo.Location{Lat: lat, Lng: lng}]; ok { - return address, nil + return &address, nil } - return "", geo.ErrNoResult + return nil, nil } diff --git a/data/geocoder_test.go b/data/geocoder_test.go index 1ca199d..c359f0a 100644 --- a/data/geocoder_test.go +++ b/data/geocoder_test.go @@ -1,34 +1,47 @@ package data_test import ( + "strings" + "testing" + "github.com/codingsince1985/geo-golang" "github.com/codingsince1985/geo-golang/data" "github.com/stretchr/testify/assert" - "testing" ) -var geocoder = data.Geocoder( - data.AddressToLocation{ - "Melbourne VIC": geo.Location{Lat: -37.814107, Lng: 144.96328}, - }, - data.LocationToAddress{ - geo.Location{Lat: -37.816742, Lng: 144.964463}: "Melbourne VIC 3000, Australia", - }, +var ( + addressFixture = geo.Address{ + FormattedAddress: "64 Elizabeth Street, Melbourne, Victoria 3000, Australia", + } + locationFixture = geo.Location{ + Lat: -37.814107, + Lng: 144.96328, + } + geocoder = data.Geocoder( + data.AddressToLocation{ + addressFixture: locationFixture, + }, + data.LocationToAddress{ + locationFixture: addressFixture, + }, + ) ) func TestGeocode(t *testing.T) { - location, err := geocoder.Geocode("Melbourne VIC") + location, err := geocoder.Geocode(addressFixture.FormattedAddress) assert.NoError(t, err) - assert.Equal(t, geo.Location{Lat: -37.814107, Lng: 144.96328}, location) + assert.Equal(t, geo.Location{Lat: -37.814107, Lng: 144.96328}, *location) } func TestReverseGeocode(t *testing.T) { - address, err := geocoder.ReverseGeocode(-37.816742, 144.964463) - assert.NoError(t, err) - assert.Equal(t, "Melbourne VIC 3000, Australia", address) + address, err := geocoder.ReverseGeocode(locationFixture.Lat, locationFixture.Lng) + assert.Nil(t, err) + assert.NotNil(t, address) + assert.True(t, strings.Contains(address.FormattedAddress, "Melbourne, Victoria 3000, Australia")) } func TestReverseGeocodeWithNoResult(t *testing.T) { - _, err := geocoder.ReverseGeocode(-37.816742, 164.964463) - assert.Equal(t, err, geo.ErrNoResult) + addr, err := geocoder.ReverseGeocode(1, 2) + assert.Nil(t, err) + assert.Nil(t, addr) } diff --git a/examples/geocoder_example.go b/examples/geocoder_example.go new file mode 100644 index 0000000..d4d9509 --- /dev/null +++ b/examples/geocoder_example.go @@ -0,0 +1,150 @@ +package main + +import ( + "fmt" + "os" + + "github.com/codingsince1985/geo-golang" + "github.com/codingsince1985/geo-golang/bing" + "github.com/codingsince1985/geo-golang/chained" + "github.com/codingsince1985/geo-golang/google" + "github.com/codingsince1985/geo-golang/here" + "github.com/codingsince1985/geo-golang/locationiq" + "github.com/codingsince1985/geo-golang/mapbox" + "github.com/codingsince1985/geo-golang/mapquest/nominatim" + "github.com/codingsince1985/geo-golang/mapquest/open" + "github.com/codingsince1985/geo-golang/opencage" + "github.com/codingsince1985/geo-golang/openstreetmap" +) + +const ( + addr = "Melbourne VIC" + lat, lng = -37.813611, 144.963056 + RADIUS = 50 + ZOOM = 18 +) + +func main() { + ExampleGeocoder() +} + +func ExampleGeocoder() { + fmt.Println("Google Geocoding API") + try(google.Geocoder(os.Getenv("GOOGLE_API_KEY"))) + + fmt.Println("Mapquest Nominatim") + try(nominatim.Geocoder(os.Getenv("MAPQUEST_NOMINATIM_KEY"))) + + fmt.Println("Mapquest Open streetmaps") + try(open.Geocoder(os.Getenv("MAPQUEST_OPEN_KEY"))) + + fmt.Println("OpenCage Data") + try(opencage.Geocoder(os.Getenv("OPENCAGE_API_KEY"))) + + fmt.Println("HERE API") + try(here.Geocoder(os.Getenv("HERE_APP_ID"), os.Getenv("HERE_APP_CODE"), RADIUS)) + + fmt.Println("Bing Geocoding API") + try(bing.Geocoder(os.Getenv("BING_API_KEY"))) + + fmt.Println("Mapbox API") + try(mapbox.Geocoder(os.Getenv("MAPBOX_API_KEY"))) + + fmt.Println("OpenStreetMap") + try(openstreetmap.Geocoder()) + + fmt.Println("LocationIQ") + try(locationiq.Geocoder(os.Getenv("LOCATIONIQ_API_KEY"), ZOOM)) + + // Chained geocoder will fallback to subsequent geocoders + fmt.Println("ChainedAPI[OpenStreetmap -> Google]") + try(chained.Geocoder( + openstreetmap.Geocoder(), + google.Geocoder(os.Getenv("GOOGLE_API_KEY")), + )) + // Output: Google Geocoding API + // Melbourne VIC location is (-37.813611, 144.963056) + // Address of (-37.813611,144.963056) is 197 Elizabeth St, Melbourne VIC 3000, Australia + // Detailed address: &geo.Address{FormattedAddress:"197 Elizabeth St, Melbourne VIC 3000, Australia", + // Street:"Elizabeth Street", HouseNumber:"197", Suburb:"", Postcode:"3000", State:"Victoria", + // StateDistrict:"Melbourne City", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} + // + // Mapquest Nominatim + // Melbourne VIC location is (-37.814218, 144.963161) + // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia + // Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Melbourne, City of Melbourne, + // Greater Melbourne, Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", + // Postcode:"3000", State:"Victoria", StateDistrict:"", County:"City of Melbourne", Country:"Australia", CountryCode:"AU", City:"Melbourne"} + // + // Mapquest Open streetmaps + // Melbourne VIC location is (-37.814218, 144.963161) + // Address of (-37.813611,144.963056) is Elizabeth Street, Melbourne, Victoria, AU + // Detailed address: &geo.Address{FormattedAddress:"Elizabeth Street, 3000, Melbourne, Victoria, AU", + // Street:"Elizabeth Street", HouseNumber:"", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", + // County:"", Country:"", CountryCode:"AU", City:"Melbourne"} + // + // OpenCage Data + // Melbourne VIC location is (-37.814217, 144.963161) + // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Melbourne VIC 3000, Australia + // Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Melbourne VIC 3000, Australia", + // Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne (3000)", Postcode:"3000", State:"Victoria", + // StateDistrict:"", County:"City of Melbourne", Country:"Australia", CountryCode:"AU", City:"Melbourne"} + // + // HERE API + // Melbourne VIC location is (-37.817530, 144.967150) + // Address of (-37.813611,144.963056) is 197 Elizabeth St, Melbourne VIC 3000, Australia + // Detailed address: &geo.Address{FormattedAddress:"197 Elizabeth St, Melbourne VIC 3000, Australia", Street:"Elizabeth St", + // HouseNumber:"197", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", County:"", Country:"Australia", + // CountryCode:"AUS", City:"Melbourne"} + // + // Bing Geocoding API + // Melbourne VIC location is (-37.824299, 144.977997) + // Address of (-37.813611,144.963056) is Elizabeth St, Melbourne, VIC 3000 + // Detailed address: &geo.Address{FormattedAddress:"Elizabeth St, Melbourne, VIC 3000", Street:"Elizabeth St", + // HouseNumber:"", Suburb:"", Postcode:"3000", State:"", StateDistrict:"", County:"", Country:"Australia", CountryCode:"", City:"Melbourne"} + // + // Mapbox API + // Melbourne VIC location is (-37.814200, 144.963200) + // Address of (-37.813611,144.963056) is Elwood Park Playground, Melbourne, Victoria 3000, Australia + // Detailed address: &geo.Address{FormattedAddress:"Elwood Park Playground, Melbourne, Victoria 3000, Australia", + // Street:"Elwood Park Playground", HouseNumber:"", Suburb:"", Postcode:"3000", State:"Victoria", StateDistrict:"", + // County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} + // + // OpenStreetMap + // Melbourne VIC location is (-37.814217, 144.963161) + // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia + // Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, + // Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", + // StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} + // + // LocationIQ + // Melbourne VIC location is (-37.814217, 144.963161) + // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia + // Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, + // Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", + // StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} + // + // ChainedAPI[OpenStreetmap -> Google] + // Melbourne VIC location is (-37.814217, 144.963161) + // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia + // Detailed address: &geo.Address{FormattedAddress:"Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, + // Victoria, 3000, Australia", Street:"Postal Lane", HouseNumber:"", Suburb:"Melbourne", Postcode:"3000", State:"Victoria", + // StateDistrict:"", County:"", Country:"Australia", CountryCode:"AU", City:"Melbourne"} +} + +func try(geocoder geo.Geocoder) { + location, _ := geocoder.Geocode(addr) + if location != nil { + fmt.Printf("%s location is (%.6f, %.6f)\n", addr, location.Lat, location.Lng) + } else { + fmt.Println("got location") + } + address, _ := geocoder.ReverseGeocode(lat, lng) + if address != nil { + fmt.Printf("Address of (%.6f,%.6f) is %s\n", lat, lng, address.FormattedAddress) + fmt.Printf("Detailed address: %#v\n", address) + } else { + fmt.Println("got address") + } + fmt.Print("\n") +} diff --git a/geocoder.go b/geocoder.go index 6dfc52d..7dbe031 100644 --- a/geocoder.go +++ b/geocoder.go @@ -3,6 +3,27 @@ package geo // Geocoder can look up (lat, long) by address and address by (lat, long) type Geocoder interface { - Geocode(address string) (Location, error) - ReverseGeocode(lat, lng float64) (string, error) + Geocode(address string) (*Location, error) + ReverseGeocode(lat, lng float64) (*Address, error) +} + +// Location is the output of Geocode +type Location struct { + Lat, Lng float64 +} + +// Address is returned by ReverseGeocode. +// This is a structured representation of an address, including its flat representation +type Address struct { + FormattedAddress string + Street string + HouseNumber string + Suburb string + Postcode string + State string + StateDistrict string + County string + Country string + CountryCode string + City string } diff --git a/geocoder_test.go b/geocoder_test.go deleted file mode 100644 index a72ee7d..0000000 --- a/geocoder_test.go +++ /dev/null @@ -1,107 +0,0 @@ -package geo_test - -import ( - "fmt" - "os" - - "github.com/codingsince1985/geo-golang" - "github.com/codingsince1985/geo-golang/bing" - "github.com/codingsince1985/geo-golang/chained" - "github.com/codingsince1985/geo-golang/google" - "github.com/codingsince1985/geo-golang/here" - "github.com/codingsince1985/geo-golang/locationiq" - "github.com/codingsince1985/geo-golang/mapbox" - "github.com/codingsince1985/geo-golang/mapquest/nominatim" - "github.com/codingsince1985/geo-golang/mapquest/open" - "github.com/codingsince1985/geo-golang/opencage" - "github.com/codingsince1985/geo-golang/openstreetmap" -) - -const ( - addr = "Melbourne VIC" - lat, lng = -37.813611, 144.963056 - RADIUS = 50 - ZOOM = 18 -) - -func ExampleGeocoder() { - fmt.Println("Google Geocoding API") - try(google.Geocoder(os.Getenv("GOOGLE_API_KEY"))) - - fmt.Println("Mapquest Nominatim") - try(nominatim.Geocoder(os.Getenv("MAPQUEST_NOMINATUM_KEY"))) - - fmt.Println("Mapquest Open streetmaps") - try(open.Geocoder(os.Getenv("MAPQUEST_OPEN_KEY"))) - - fmt.Println("OpenCage Data") - try(opencage.Geocoder(os.Getenv("OPENCAGE_API_KEY"))) - - fmt.Println("HERE API") - try(here.Geocoder(os.Getenv("HERE_APP_ID"), os.Getenv("HERE_APP_CODE"), RADIUS)) - - fmt.Println("Bing Geocoding API") - try(bing.Geocoder(os.Getenv("BING_API_KEY"))) - - fmt.Println("Mapbox API") - try(mapbox.Geocoder(os.Getenv("MAPBOX_API_KEY"))) - - fmt.Println("OpenStreetMap") - try(openstreetmap.Geocoder()) - - fmt.Println("LocationIQ") - try(locationiq.Geocoder(os.Getenv("LOCATIONIQ_API_KEY"), ZOOM)) - - // Chained geocoder will fallback to subsequent geocoders - fmt.Println("ChainedAPI[OpenStreetmap -> Google]") - try(chained.Geocoder( - openstreetmap.Geocoder(), - google.Geocoder(os.Getenv("GOOGLE_API_KEY")), - )) - // Output: Google Geocoding API - // Melbourne VIC location is (-37.813628, 144.963058) - // Address of (-37.813611,144.963056) is 350 Bourke St, Melbourne VIC 3004, Australia - // - // Mapquest Nominatim - // Melbourne VIC location is (-37.814218, 144.963161) - // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia - // - // Mapquest Open streetmaps - // Melbourne VIC location is (-37.814218, 144.963161) - // Address of (-37.813611,144.963056) is Elizabeth Street, Melbourne, Victoria, AU - // - // OpenCage Data - // Melbourne VIC location is (-37.814217, 144.963161) - // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Melbourne VIC 3000, Australia - // - // HERE API - // Melbourne VIC location is (-37.817530, 144.967150) - // Address of (-37.813611,144.963056) is 197 Elizabeth St, Melbourne VIC 3000, Australia - // - // Bing Geocoding API - // Melbourne VIC location is (-37.824299, 144.977997) - // Address of (-37.813611,144.963056) is Elizabeth St, Melbourne, VIC 3000 - // - // Mapbox API - // Melbourne VIC location is (-37.814200, 144.963200) - // Address of (-37.813611,144.963056) is Elwood Park Playground, Melbourne, Victoria 3000, Australia - // - // OpenStreetMap - // Melbourne VIC location is (-37.814217, 144.963161) - // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia - // - // LocationIQ - // Melbourne VIC location is (-37.814217, 144.963161) - // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia - // - // ChainedAPI[OpenStreetmap -> Google] - // Melbourne VIC location is (-37.814217, 144.963161) - // Address of (-37.813611,144.963056) is Melbourne's GPO, Postal Lane, Chinatown, Melbourne, City of Melbourne, Greater Melbourne, Victoria, 3000, Australia -} - -func try(geocoder geo.Geocoder) { - location, _ := geocoder.Geocode(addr) - fmt.Printf("%s location is (%.6f, %.6f)\n", addr, location.Lat, location.Lng) - address, _ := geocoder.ReverseGeocode(lat, lng) - fmt.Printf("Address of (%.6f,%.6f) is %s\n\n", lat, lng, address) -} diff --git a/google/geocoder.go b/google/geocoder.go index e5b83fd..0b82d3b 100644 --- a/google/geocoder.go +++ b/google/geocoder.go @@ -4,21 +4,42 @@ package google import ( "fmt" - geo "github.com/codingsince1985/geo-golang" + + "github.com/codingsince1985/geo-golang" ) type ( baseURL string geocodeResponse struct { Results []struct { - FormattedAddress string `json:"formatted_address"` - Geometry struct { + FormattedAddress string `json:"formatted_address"` + AddressComponents []googleAddressComponent `json:"address_components"` + Geometry struct { Location geo.Location } } + Status string `json: "status"` + } + googleAddressComponent struct { + LongName string `json:"long_name"` + ShortName string `json:"short_name"` + Types []string `json:"types"` } ) +const ( + statusOK = "OK" + statusNoResults = "ZERO_RESULTS" + componentTypeHouseNumber = "street_number" + componentTypeStreetName = "route" + componentTypeSuburb = "sublocality" + componentTypeLocality = "locality" + componentTypeStateDistrict = "administrative_area_level_2" + componentTypeState = "administrative_area_level_1" + componentTypeCountry = "country" + componentTypePostcode = "postal_code" +) + // Geocoder constructs Google geocoder func Geocoder(apiKey string, baseURLs ...string) geo.Geocoder { return geo.HTTPGeocoder{ @@ -37,19 +58,71 @@ func getUrl(apiKey string, baseURLs ...string) string { func (b baseURL) GeocodeURL(address string) string { return string(b) + "address=" + address } func (b baseURL) ReverseGeocodeURL(l geo.Location) string { - return string(b) + fmt.Sprintf("latlng=%f,%f", l.Lat, l.Lng) + return string(b) + fmt.Sprintf("result_type=street_address&latlng=%f,%f", l.Lat, l.Lng) } -func (r *geocodeResponse) Location() geo.Location { - if len(r.Results) > 0 { - return r.Results[0].Geometry.Location +func (r *geocodeResponse) Location() (*geo.Location, error) { + if r.Status == statusNoResults { + return nil, nil + } else if r.Status != statusOK { + return nil, fmt.Errorf("geocoding error: %s", r.Status) } - return geo.Location{} + + return &r.Results[0].Geometry.Location, nil } -func (r *geocodeResponse) Address() string { - if len(r.Results) > 0 { - return r.Results[0].FormattedAddress +func (r *geocodeResponse) Address() (*geo.Address, error) { + if r.Status == statusNoResults { + return nil, nil + } else if r.Status != statusOK { + return nil, fmt.Errorf("reverse geocoding error: %s", r.Status) + } + + if len(r.Results) == 0 || len(r.Results[0].AddressComponents) == 0 { + return nil, nil } - return "" + + addr := parseGoogleResult(r) + + return addr, nil +} + +func parseGoogleResult(r *geocodeResponse) *geo.Address { + addr := &geo.Address{} + res := r.Results[0] + addr.FormattedAddress = res.FormattedAddress +OuterLoop: + for _, comp := range res.AddressComponents { + for _, typ := range comp.Types { + switch typ { + case componentTypeHouseNumber: + addr.HouseNumber = comp.LongName + continue OuterLoop + case componentTypeStreetName: + addr.Street = comp.LongName + continue OuterLoop + case componentTypeSuburb: + addr.Suburb = comp.LongName + continue OuterLoop + case componentTypeLocality: + addr.City = comp.LongName + continue OuterLoop + case componentTypeStateDistrict: + addr.StateDistrict = comp.LongName + continue OuterLoop + case componentTypeState: + addr.State = comp.LongName + continue OuterLoop + case componentTypeCountry: + addr.Country = comp.LongName + addr.CountryCode = comp.ShortName + continue OuterLoop + case componentTypePostcode: + addr.Postcode = comp.LongName + continue OuterLoop + } + } + } + + return addr } diff --git a/google/geocoder_test.go b/google/geocoder_test.go index e6d6aa2..7a4a9b3 100644 --- a/google/geocoder_test.go +++ b/google/geocoder_test.go @@ -1,14 +1,15 @@ package google_test import ( - "github.com/codingsince1985/geo-golang" - "github.com/codingsince1985/geo-golang/google" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "os" "strings" "testing" + + "github.com/codingsince1985/geo-golang" + "github.com/codingsince1985/geo-golang/google" + "github.com/stretchr/testify/assert" ) var token = os.Getenv("GOOGLE_API_KEY") @@ -20,7 +21,7 @@ func TestGeocode(t *testing.T) { geocoder := google.Geocoder(token, ts.URL+"/") location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") assert.NoError(t, err) - assert.Equal(t, geo.Location{Lat: -37.8137683, Lng: 144.9718448}, location) + assert.Equal(t, geo.Location{Lat: -37.8137683, Lng: 144.9718448}, *location) } func TestReverseGeocode(t *testing.T) { @@ -30,7 +31,8 @@ func TestReverseGeocode(t *testing.T) { geocoder := google.Geocoder(token, ts.URL+"/") address, err := geocoder.ReverseGeocode(-37.8137683, 144.9718448) assert.NoError(t, err) - assert.True(t, strings.HasPrefix(address, "60 Collins St")) + assert.True(t, strings.HasPrefix(address.FormattedAddress, "60 Collins St")) + assert.True(t, strings.HasPrefix(address.Street, "Collins St")) } func TestReverseGeocodeWithNoResult(t *testing.T) { @@ -38,8 +40,9 @@ func TestReverseGeocodeWithNoResult(t *testing.T) { defer ts.Close() geocoder := google.Geocoder(token, ts.URL+"/") - _, err := geocoder.ReverseGeocode(-37.8137683, 164.9718448) - assert.Equal(t, err, geo.ErrNoResult) + addr, err := geocoder.ReverseGeocode(-37.8137683, 164.9718448) + assert.Nil(t, err) + assert.Nil(t, addr) } func testServer(response string) *httptest.Server { diff --git a/here/geocoder.go b/here/geocoder.go index 952f4bd..77cc450 100644 --- a/here/geocoder.go +++ b/here/geocoder.go @@ -3,6 +3,7 @@ package here import ( "fmt" + "github.com/codingsince1985/geo-golang" ) @@ -17,7 +18,19 @@ type ( Latitude, Longitude float64 } Address struct { - Label string + Label string + Country string + State string + County string + City string + District string + Street string + HouseNumber string + PostalCode string + AdditionalData []struct { + Key string + Value string + } } } } @@ -26,6 +39,12 @@ type ( } ) +const ( + KeyCountryName = "CountryName" + KeyStateName = "StateName" + KeyCountyName = "CountyName" +) + var r = 100 // Geocoder constructs HERE geocoder @@ -62,17 +81,39 @@ func (b baseURL) ReverseGeocodeURL(l geo.Location) string { return b.forReverseGeocode + fmt.Sprintf("&prox=%f,%f,%d", l.Lat, l.Lng, r) } -func (r *geocodeResponse) Location() geo.Location { - if len(r.Response.View) > 0 { - p := r.Response.View[0].Result[0].Location.DisplayPosition - return geo.Location{p.Latitude, p.Longitude} +func (r *geocodeResponse) Location() (*geo.Location, error) { + if len(r.Response.View) == 0 { + return nil, nil } - return geo.Location{} + p := r.Response.View[0].Result[0].Location.DisplayPosition + return &geo.Location{ + Lat: p.Latitude, + Lng: p.Longitude, + }, nil } -func (r *geocodeResponse) Address() string { - if len(r.Response.View) > 0 { - return r.Response.View[0].Result[0].Location.Address.Label +func (r *geocodeResponse) Address() (*geo.Address, error) { + if len(r.Response.View) == 0 || len(r.Response.View[0].Result) == 0 { + return nil, nil + } + + res := r.Response.View[0].Result[0].Location.Address + + addr := &geo.Address{ + FormattedAddress: res.Label, + Street: res.Street, + HouseNumber: res.HouseNumber, + City: res.City, + Postcode: res.PostalCode, + CountryCode: res.Country, + } + for _, v := range res.AdditionalData { + switch v.Key { + case KeyCountryName: + addr.Country = v.Value + case KeyStateName: + addr.State = v.Value + } } - return "" + return addr, nil } diff --git a/here/geocoder_test.go b/here/geocoder_test.go index 3a5e5c6..6d0a8e7 100644 --- a/here/geocoder_test.go +++ b/here/geocoder_test.go @@ -1,14 +1,15 @@ package here_test import ( - "github.com/codingsince1985/geo-golang" - "github.com/codingsince1985/geo-golang/here" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "os" "strings" "testing" + + "github.com/codingsince1985/geo-golang" + "github.com/codingsince1985/geo-golang/here" + "github.com/stretchr/testify/assert" ) var appID = os.Getenv("HERE_APP_ID") @@ -21,7 +22,7 @@ func TestGeocode(t *testing.T) { geocoder := here.Geocoder(appID, appCode, 100, ts.URL+"/") location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") assert.NoError(t, err) - assert.Equal(t, geo.Location{Lat: -37.81375, Lng: 144.97176}, location) + assert.Equal(t, geo.Location{Lat: -37.81375, Lng: 144.97176}, *location) } func TestReverseGeocode(t *testing.T) { @@ -31,7 +32,7 @@ func TestReverseGeocode(t *testing.T) { geocoder := here.Geocoder(appID, appCode, 100, ts.URL+"/") address, err := geocoder.ReverseGeocode(-37.81375, 144.97176) assert.NoError(t, err) - assert.True(t, strings.HasPrefix(address, "56-64 Collins St")) + assert.True(t, strings.HasPrefix(address.FormattedAddress, "56-64 Collins St")) } func TestReverseGeocodeWithNoResult(t *testing.T) { @@ -39,8 +40,8 @@ func TestReverseGeocodeWithNoResult(t *testing.T) { defer ts.Close() geocoder := here.Geocoder(appID, appCode, 100, ts.URL+"/") - _, err := geocoder.ReverseGeocode(-37.81375, 164.97176) - assert.Equal(t, err, geo.ErrNoResult) + addr, _ := geocoder.ReverseGeocode(-37.81375, 164.97176) + assert.Nil(t, addr) } func testServer(response string) *httptest.Server { diff --git a/http_geocoder.go b/http_geocoder.go index 8af3944..65450d3 100644 --- a/http_geocoder.go +++ b/http_geocoder.go @@ -1,6 +1,7 @@ package geo import ( + "context" "encoding/json" "errors" "io/ioutil" @@ -11,19 +12,12 @@ import ( "time" ) -var timeout = time.Second * 8 +// Default timeout for the request execution +const DefaultTimeout = time.Second * 8 // ErrTimeout occurs when no response returned within timeoutInSeconds var ErrTimeout = errors.New("TIMEOUT") -// ErrNoResult occurs when no result returned -var ErrNoResult = errors.New("NO_RESULT") - -// Location is the output of Geocode -type Location struct { - Lat, Lng float64 -} - // EndpointBuilder defines functions that build urls for geocode/reverse geocode type EndpointBuilder interface { GeocodeURL(string) string @@ -35,8 +29,8 @@ type ResponseParserFactory func() ResponseParser // ResponseParser defines functions that parse response of geocode/reverse geocode type ResponseParser interface { - Location() Location - Address() string + Location() (*Location, error) + Address() (*Address, error) } // HTTPGeocoder has EndpointBuilder and ResponseParser @@ -46,66 +40,104 @@ type HTTPGeocoder struct { } // Geocode returns location for address -func (g HTTPGeocoder) Geocode(address string) (Location, error) { - ch := make(chan Location, 1) - go func() { - responseParser := g.ResponseParserFactory() - response(g.GeocodeURL(url.QueryEscape(address)), responseParser) - ch <- responseParser.Location() - }() +func (g HTTPGeocoder) Geocode(address string) (*Location, error) { + responseParser := g.ResponseParserFactory() + + ctx, cancel := context.WithTimeout(context.TODO(), DefaultTimeout) + defer cancel() + + type geoResp struct { + l *Location + e error + } + ch := make(chan geoResp, 1) + + go func(ch chan geoResp) { + if err := response(ctx, g.GeocodeURL(url.QueryEscape(address)), responseParser); err != nil { + ch <- geoResp{ + l: nil, + e: err, + } + } + + loc, err := responseParser.Location() + ch <- geoResp{ + l: loc, + e: err, + } + }(ch) select { - case location := <-ch: - return location, anyError(location) - case <-time.After(timeout): - return Location{}, ErrTimeout + case <-ctx.Done(): + return nil, ErrTimeout + case res := <-ch: + return res.l, res.e } } // ReverseGeocode returns address for location -func (g HTTPGeocoder) ReverseGeocode(lat, lng float64) (string, error) { - ch := make(chan string, 1) - go func() { - responseParser := g.ResponseParserFactory() - response(g.ReverseGeocodeURL(Location{lat, lng}), responseParser) - ch <- responseParser.Address() - }() +func (g HTTPGeocoder) ReverseGeocode(lat, lng float64) (*Address, error) { + responseParser := g.ResponseParserFactory() - select { - case address := <-ch: - return address, anyError(address) - case <-time.After(timeout): - return "", ErrTimeout + ctx, cancel := context.WithTimeout(context.TODO(), DefaultTimeout) + defer cancel() + + type revResp struct { + a *Address + e error } -} + ch := make(chan revResp, 1) -// Response gets response from url -func response(url string, obj ResponseParser) { - if req, err := http.NewRequest("GET", url, nil); err == nil { - if resp, err := (&http.Client{}).Do(req); err == nil { - defer resp.Body.Close() - if data, err := ioutil.ReadAll(resp.Body); err == nil { - // TODO: don't swallow json unmarshal errors - // currently it just treats an empty response as a ErrNoResult which - // is fine for now but we should have some logging or something to indicate - // failed json unmarshal - json.Unmarshal([]byte(strings.Trim(string(data), " []")), obj) + go func(ch chan revResp) { + if err := response(ctx, g.ReverseGeocodeURL(Location{lat, lng}), responseParser); err != nil { + ch <- revResp{ + a: nil, + e: err, } } + + addr, err := responseParser.Address() + ch <- revResp{ + a: addr, + e: err, + } + }(ch) + + select { + case <-ctx.Done(): + return nil, ErrTimeout + case res := <-ch: + return res.a, res.e } } -func anyError(v interface{}) error { - switch v := v.(type) { - case Location: - if v.Lat == 0 && v.Lng == 0 { - return ErrNoResult - } - case string: - if v == "" { - return ErrNoResult - } +// Response gets response from url +func response(ctx context.Context, url string, obj ResponseParser) error { + req, err := http.NewRequest(http.MethodGet, url, nil) + if err != nil { + return err } + req = req.WithContext(ctx) + + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + data, err := ioutil.ReadAll(resp.Body) + if err != nil { + return err + } + + body := strings.Trim(string(data), " []") + if body == "" { + return nil + } + if err := json.Unmarshal([]byte(body), obj); err != nil { + return err + } + return nil } diff --git a/locationiq/geocoder.go b/locationiq/geocoder.go index 2ff5fa4..8e351c4 100644 --- a/locationiq/geocoder.go +++ b/locationiq/geocoder.go @@ -3,7 +3,10 @@ package locationiq import ( "fmt" + "strings" + "github.com/codingsince1985/geo-golang" + "github.com/codingsince1985/geo-golang/osm" ) type baseURL string @@ -11,20 +14,7 @@ type baseURL string type geocodeResponse struct { DisplayName string `json:"display_name"` Lat, Lon, Error string - Addr locationiqAddress `json:"address"` -} - -type locationiqAddress struct { - HouseNumber string `json:"house_number"` - Suburb string `json:"suburb"` - City string `json:"city"` - County string `json:"county"` - Country string `json:"country"` - CountryCode string `json:"country_code"` - Road string `json:"road"` - State string `json:"state"` - StateDistrict string `json:"state_district"` - Postcode string `json:"postcode"` + Addr osm.Address `json:"address"` } const ( @@ -70,19 +60,36 @@ func (b baseURL) ReverseGeocodeURL(l geo.Location) string { return string(b) + "reverse.php?key=" + key + fmt.Sprintf("&format=json&lat=%f&lon=%f&zoom=%d", l.Lat, l.Lng, zoom) } -func (r *geocodeResponse) Location() geo.Location { - l := geo.Location{} - // In case of empty response from LocationIQ or any other error we get zero values - if r.Lat != "" && r.Lon != "" { - l.Lat = geo.ParseFloat(r.Lat) - l.Lng = geo.ParseFloat(r.Lon) +func (r *geocodeResponse) Location() (*geo.Location, error) { + if r.Error != "" { + return nil, fmt.Errorf("geocoding error: %s", r.Error) + } + + // no result + if r.Lat == "" || r.Lon == "" { + return nil, nil } - return l + + return &geo.Location{ + Lat: geo.ParseFloat(r.Lat), + Lng: geo.ParseFloat(r.Lon), + }, nil } -func (r *geocodeResponse) Address() string { +func (r *geocodeResponse) Address() (*geo.Address, error) { if r.Error != "" { - return "" + return nil, fmt.Errorf("reverse geocoding error: %s", r.Error) } - return r.DisplayName + + return &geo.Address{ + FormattedAddress: r.DisplayName, + Street: r.Addr.Street(), + HouseNumber: r.Addr.HouseNumber, + City: r.Addr.Locality(), + Postcode: r.Addr.Postcode, + Suburb: r.Addr.Suburb, + State: r.Addr.State, + Country: r.Addr.Country, + CountryCode: strings.ToUpper(r.Addr.CountryCode), + }, nil } diff --git a/locationiq/geocoder_test.go b/locationiq/geocoder_test.go index 347017c..7f5b04c 100644 --- a/locationiq/geocoder_test.go +++ b/locationiq/geocoder_test.go @@ -37,14 +37,12 @@ func TestGeocodeYieldsNoResult(t *testing.T) { gc := Geocoder("foobar", 18, ts.URL+"/") l, err := gc.Geocode("Seidlstraße 26, 80335 München") - if err == nil { - t.Error("Got nil error") - } - if l.Lat != 0 { - t.Errorf("Expected latitude: %d, got: %f", 0, l.Lat) + if l != nil { + t.Errorf("Expected nil, got %#v", l) } - if l.Lng != 0 { - t.Errorf("Expected longitude: %d, got: %f", 0, l.Lat) + + if err != nil { + t.Errorf("Expected nil error, got %v", err) } } @@ -58,7 +56,7 @@ func TestReverseGeocodeYieldsResult(t *testing.T) { if err != nil { t.Errorf("Expected nil error, got %v", err) } - if !strings.HasPrefix(addr, "26, Seidlstraße") { + if !strings.HasPrefix(addr.FormattedAddress, "26, Seidlstraße") { t.Errorf("Expected address string starting with %s, got string: %s", "26, Seidlstraße", addr) } } @@ -73,8 +71,8 @@ func TestReverseGeocodeYieldsNoResult(t *testing.T) { if err == nil { t.Error("Expected error, got nil") } - if addr != "" { - t.Errorf("Expected empty string as address, got: %s", addr) + if addr != nil { + t.Errorf("Expected nil as address, got: %s", addr) } } diff --git a/mapbox/geocoder.go b/mapbox/geocoder.go index 2a4f02b..62d22c7 100644 --- a/mapbox/geocoder.go +++ b/mapbox/geocoder.go @@ -3,8 +3,9 @@ package mapbox import ( "fmt" - "github.com/codingsince1985/geo-golang" "strings" + + "github.com/codingsince1985/geo-golang" ) type ( @@ -13,10 +14,26 @@ type ( Features []struct { PlaceName string `json:"place_name"` Center [2]float64 + Text string `json:"text"` // usually street name + Address string `json:"address"` // potentially house number + Context []struct { + Text string `json:"text"` + Id string `json:"id"` + ShortCode string `json:"short_code"` + Wikidata string `json:"wikidata"` + } } + Message string `json:"message"` } ) +const ( + mapboxPrefixLocality = "place" + mapboxPrefixPostcode = "postcode" + mapboxPrefixState = "region" + mapboxPrefixCountry = "country" +) + // Geocoder constructs Mapbox geocoder func Geocoder(token string, baseURLs ...string) geo.Geocoder { return geo.HTTPGeocoder{ @@ -29,7 +46,7 @@ func getUrl(token string, baseURLs ...string) string { if len(baseURLs) > 0 { return baseURLs[0] } - return "https://api.mapbox.com/geocoding/v5/mapbox.places/*.json?access_token=" + token + return "https://api.mapbox.com/geocoding/v5/mapbox.places/*.json?limit=1&access_token=" + token } func (b baseURL) GeocodeURL(address string) string { return strings.Replace(string(b), "*", address, 1) } @@ -38,17 +55,54 @@ func (b baseURL) ReverseGeocodeURL(l geo.Location) string { return strings.Replace(string(b), "*", fmt.Sprintf("%+f,%+f", l.Lng, l.Lat), 1) } -func (r *geocodeResponse) Location() geo.Location { - if len(r.Features) > 0 { - g := r.Features[0] - return geo.Location{g.Center[1], g.Center[0]} +func (r *geocodeResponse) Location() (*geo.Location, error) { + if len(r.Features) == 0 { + // error in response + if r.Message != "" { + return nil, fmt.Errorf("reverse geocoding error: %s", r.Message) + } else { // no results + return nil, nil + } + } + + g := r.Features[0] + return &geo.Location{ + Lat: g.Center[1], + Lng: g.Center[0], + }, nil +} + +func (r *geocodeResponse) Address() (*geo.Address, error) { + if len(r.Features) == 0 { + // error in response + if r.Message != "" { + return nil, fmt.Errorf("reverse geocoding error: %s", r.Message) + } else { // no results + return nil, nil + } } - return geo.Location{} + + return parseMapboxResponse(r), nil } -func (r *geocodeResponse) Address() string { - if len(r.Features) > 0 { - return r.Features[0].PlaceName +func parseMapboxResponse(r *geocodeResponse) *geo.Address { + addr := &geo.Address{} + f := r.Features[0] + addr.FormattedAddress = f.PlaceName + addr.Street = f.Text + addr.HouseNumber = f.Address + for _, c := range f.Context { + if strings.HasPrefix(c.Id, mapboxPrefixLocality) { + addr.City = c.Text + } else if strings.HasPrefix(c.Id, mapboxPrefixPostcode) { + addr.Postcode = c.Text + } else if strings.HasPrefix(c.Id, mapboxPrefixState) { + addr.State = c.Text + } else if strings.HasPrefix(c.Id, mapboxPrefixCountry) { + addr.Country = c.Text + addr.CountryCode = strings.ToUpper(c.ShortCode) + } } - return "" + + return addr } diff --git a/mapbox/geocoder_test.go b/mapbox/geocoder_test.go index 1eeba23..17a065a 100644 --- a/mapbox/geocoder_test.go +++ b/mapbox/geocoder_test.go @@ -1,15 +1,15 @@ package mapbox_test import ( - "fmt" - "github.com/codingsince1985/geo-golang" - "github.com/codingsince1985/geo-golang/mapbox" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "os" "strings" "testing" + + "github.com/codingsince1985/geo-golang" + "github.com/codingsince1985/geo-golang/mapbox" + "github.com/stretchr/testify/assert" ) var token = os.Getenv("MAPBOX_API_KEY") @@ -21,7 +21,7 @@ func TestGeocode(t *testing.T) { geocoder := mapbox.Geocoder(token, ts.URL+"/") location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") assert.NoError(t, err) - assert.Equal(t, geo.Location{Lat: -37.813754, Lng: 144.971756}, location) + assert.Equal(t, geo.Location{Lat: -37.813754, Lng: 144.971756}, *location) } func TestReverseGeocode(t *testing.T) { @@ -31,8 +31,7 @@ func TestReverseGeocode(t *testing.T) { geocoder := mapbox.Geocoder(token, ts.URL+"/") address, err := geocoder.ReverseGeocode(-37.813754, 144.971756) assert.NoError(t, err) - fmt.Println(address) - assert.True(t, strings.Index(address, "60 Collins St") >= 0) + assert.True(t, strings.Index(address.FormattedAddress, "60 Collins St") >= 0) } func TestReverseGeocodeWithNoResult(t *testing.T) { @@ -40,8 +39,9 @@ func TestReverseGeocodeWithNoResult(t *testing.T) { defer ts.Close() geocoder := mapbox.Geocoder(token, ts.URL+"/") - _, err := geocoder.ReverseGeocode(-37.813754, 164.971756) - assert.Equal(t, err, geo.ErrNoResult) + addr, err := geocoder.ReverseGeocode(-37.813754, 164.971756) + assert.Nil(t, err) + assert.Nil(t, addr) } func testServer(response string) *httptest.Server { diff --git a/mapquest/nominatim/geocoder.go b/mapquest/nominatim/geocoder.go index 9bf062c..2060376 100644 --- a/mapquest/nominatim/geocoder.go +++ b/mapquest/nominatim/geocoder.go @@ -3,14 +3,19 @@ package nominatim import ( "fmt" + "strings" + "github.com/codingsince1985/geo-golang" + "github.com/codingsince1985/geo-golang/osm" ) type ( - baseURL string + baseURL string + geocodeResponse struct { DisplayName string `json:"display_name"` Lat, Lon, Error string + Addr osm.Address `json:"address"` } ) @@ -40,16 +45,32 @@ func (b baseURL) ReverseGeocodeURL(l geo.Location) string { return string(b) + "reverse.php?key=" + key + fmt.Sprintf("&format=json&lat=%f&lon=%f", l.Lat, l.Lng) } -func (r *geocodeResponse) Location() geo.Location { - if r.Error == "" { - return geo.Location{geo.ParseFloat(r.Lat), geo.ParseFloat(r.Lon)} +func (r *geocodeResponse) Location() (*geo.Location, error) { + if r.Error != "" { + return nil, fmt.Errorf("geocode error: %s", r.Error) } - return geo.Location{} + + return &geo.Location{ + Lat: geo.ParseFloat(r.Lat), + Lng: geo.ParseFloat(r.Lon), + }, nil } -func (r *geocodeResponse) Address() string { - if r.Error == "" { - return r.DisplayName +func (r *geocodeResponse) Address() (*geo.Address, error) { + if r.Error != "" { + return nil, fmt.Errorf("reverse geocode error: %s", r.Error) } - return "" + + return &geo.Address{ + FormattedAddress: r.DisplayName, + HouseNumber: r.Addr.HouseNumber, + Street: r.Addr.Street(), + Suburb: r.Addr.Suburb, + City: r.Addr.Locality(), + State: r.Addr.State, + County: r.Addr.County, + Postcode: r.Addr.Postcode, + Country: r.Addr.Country, + CountryCode: strings.ToUpper(r.Addr.CountryCode), + }, nil } diff --git a/mapquest/nominatim/geocoder_test.go b/mapquest/nominatim/geocoder_test.go index bcde31c..66e4f53 100644 --- a/mapquest/nominatim/geocoder_test.go +++ b/mapquest/nominatim/geocoder_test.go @@ -1,14 +1,15 @@ package nominatim_test import ( - "github.com/codingsince1985/geo-golang" - "github.com/codingsince1985/geo-golang/mapquest/nominatim" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "os" "strings" "testing" + + "github.com/codingsince1985/geo-golang" + "github.com/codingsince1985/geo-golang/mapquest/nominatim" + "github.com/stretchr/testify/assert" ) var key = os.Getenv("MAPQUEST_NOMINATUM_KEY") @@ -20,7 +21,7 @@ func TestGeocode(t *testing.T) { geocoder := nominatim.Geocoder(key, ts.URL+"/") location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") assert.NoError(t, err) - assert.Equal(t, geo.Location{Lat: -37.8137433689794, Lng: 144.971745104488}, location) + assert.Equal(t, geo.Location{Lat: -37.8137433689794, Lng: 144.971745104488}, *location) } func TestReverseGeocode(t *testing.T) { @@ -28,9 +29,9 @@ func TestReverseGeocode(t *testing.T) { defer ts.Close() geocoder := nominatim.Geocoder(key, ts.URL+"/") - address, err := geocoder.ReverseGeocode(-37.8137433689794, 144.971745104488) + addr, err := geocoder.ReverseGeocode(-37.8137433689794, 144.971745104488) assert.NoError(t, err) - assert.True(t, strings.HasPrefix(address, "Reserve Bank of Australia")) + assert.True(t, strings.HasPrefix(addr.FormattedAddress, "Reserve Bank of Australia")) } func TestReverseGeocodeWithNoResult(t *testing.T) { @@ -38,8 +39,10 @@ func TestReverseGeocodeWithNoResult(t *testing.T) { defer ts.Close() geocoder := nominatim.Geocoder(key, ts.URL+"/") - _, err := geocoder.ReverseGeocode(-37.8137433689794, 164.971745104488) - assert.Equal(t, err, geo.ErrNoResult) + //geocoder := nominatim.Geocoder(key) + addr, err := geocoder.ReverseGeocode(-37.8137433689794, 164.971745104488) + assert.NotNil(t, err) + assert.Nil(t, addr) } func testServer(response string) *httptest.Server { diff --git a/mapquest/open/geocoder.go b/mapquest/open/geocoder.go index 566755d..621a08f 100644 --- a/mapquest/open/geocoder.go +++ b/mapquest/open/geocoder.go @@ -3,8 +3,9 @@ package open import ( "fmt" - "github.com/codingsince1985/geo-golang" "strings" + + "github.com/codingsince1985/geo-golang" ) type ( @@ -12,8 +13,17 @@ type ( geocodeResponse struct { Results []struct { Locations []struct { - LatLng geo.Location - Street, AdminArea5, AdminArea3, AdminArea1 string + LatLng struct { + Lat float64 + Lng float64 + } + PostalCode string + Street string + AdminArea6 string // neighbourhood + AdminArea5 string // city + AdminArea4 string // county + AdminArea3 string // state + AdminArea1 string // country (ISO 3166-1 alpha-2 code) } } } @@ -43,12 +53,37 @@ func (b baseURL) ReverseGeocodeURL(l geo.Location) string { return strings.Replace(string(b), "*", "reverse", 1) + fmt.Sprintf("%f,%f", l.Lat, l.Lng) } -func (r *geocodeResponse) Location() geo.Location { return r.Results[0].Locations[0].LatLng } +func (r *geocodeResponse) Location() (*geo.Location, error) { + if len(r.Results) == 0 || len(r.Results[0].Locations) == 0 { + return nil, nil + } + + loc := r.Results[0].Locations[0].LatLng + return &geo.Location{ + Lat: loc.Lat, + Lng: loc.Lng, + }, nil +} + +func (r *geocodeResponse) Address() (*geo.Address, error) { + if len(r.Results) == 0 || len(r.Results[0].Locations) == 0 { + return nil, nil + } -func (r *geocodeResponse) Address() string { p := r.Results[0].Locations[0] - if p.AdminArea1 != "" { - return p.Street + ", " + p.AdminArea5 + ", " + p.AdminArea3 + ", " + p.AdminArea1 + if p.Street == "" || p.AdminArea5 == "" { + return nil, nil } - return "" + + formattedAddress := p.Street + ", " + p.PostalCode + ", " + p.AdminArea5 + ", " + p.AdminArea3 + ", " + p.AdminArea1 + return &geo.Address{ + FormattedAddress: formattedAddress, + Street: p.Street, + Suburb: p.AdminArea6, + Postcode: p.PostalCode, + City: p.AdminArea5, + County: p.AdminArea4, + State: p.AdminArea3, + CountryCode: p.AdminArea1, + }, nil } diff --git a/mapquest/open/geocoder_test.go b/mapquest/open/geocoder_test.go index ebd4a41..efddea1 100644 --- a/mapquest/open/geocoder_test.go +++ b/mapquest/open/geocoder_test.go @@ -1,14 +1,15 @@ package open_test import ( - "github.com/codingsince1985/geo-golang" - "github.com/codingsince1985/geo-golang/mapquest/open" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "os" "strings" "testing" + + "github.com/codingsince1985/geo-golang" + "github.com/codingsince1985/geo-golang/mapquest/open" + "github.com/stretchr/testify/assert" ) var key = os.Getenv("MAPQUEST_OPEN_KEY") @@ -20,7 +21,7 @@ func TestGeocode(t *testing.T) { geocoder := open.Geocoder(key, ts.URL+"/") location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") assert.NoError(t, err) - assert.Equal(t, geo.Location{Lat: -37.813743, Lng: 144.971745}, location) + assert.Equal(t, geo.Location{Lat: -37.813743, Lng: 144.971745}, *location) } func TestReverseGeocode(t *testing.T) { @@ -30,7 +31,7 @@ func TestReverseGeocode(t *testing.T) { geocoder := open.Geocoder(key, ts.URL+"/") address, err := geocoder.ReverseGeocode(-37.813743, 144.971745) assert.NoError(t, err) - assert.True(t, strings.HasPrefix(address, "Exhibition Street")) + assert.True(t, strings.HasPrefix(address.FormattedAddress, "Exhibition Street")) } func TestReverseGeocodeWithNoResult(t *testing.T) { @@ -38,8 +39,10 @@ func TestReverseGeocodeWithNoResult(t *testing.T) { defer ts.Close() geocoder := open.Geocoder(key, ts.URL+"/") - _, err := geocoder.ReverseGeocode(-37.813743, 164.971745) - assert.Equal(t, err, geo.ErrNoResult) + //geocoder := open.Geocoder(key) + addr, err := geocoder.ReverseGeocode(-37.813743, 164.971745) + assert.Nil(t, err) + assert.Nil(t, addr) } func testServer(response string) *httptest.Server { diff --git a/opencage/geocoder.go b/opencage/geocoder.go index 0017b3f..eea5d32 100644 --- a/opencage/geocoder.go +++ b/opencage/geocoder.go @@ -3,15 +3,24 @@ package opencage import ( "fmt" + "strings" + "github.com/codingsince1985/geo-golang" + "github.com/codingsince1985/geo-golang/osm" ) type ( - baseURL string + baseURL string + geocodeResponse struct { Results []struct { - Formatted string - Geometry geo.Location + Formatted string + Geometry geo.Location + Components osm.Address + } + Status struct { + Code int + Message string } } ) @@ -37,16 +46,41 @@ func (b baseURL) ReverseGeocodeURL(l geo.Location) string { return string(b) + fmt.Sprintf("%+f,%+f", l.Lat, l.Lng) } -func (r *geocodeResponse) Location() geo.Location { - if len(r.Results) > 0 { - return r.Results[0].Geometry +func (r *geocodeResponse) Location() (*geo.Location, error) { + if r.Status.Code >= 400 { + return nil, fmt.Errorf("geocoding error: %s", r.Status.Message) + } + if len(r.Results) == 0 { + return nil, nil } - return geo.Location{} + + return &geo.Location{ + Lat: r.Results[0].Geometry.Lat, + Lng: r.Results[0].Geometry.Lng, + }, nil } -func (r *geocodeResponse) Address() string { - if len(r.Results) > 0 { - return r.Results[0].Formatted +func (r *geocodeResponse) Address() (*geo.Address, error) { + if r.Status.Code >= 400 { + return nil, fmt.Errorf("geocoding error: %s", r.Status.Message) + } + if len(r.Results) == 0 { + return nil, nil } - return "" + + addr := r.Results[0].Components + + return &geo.Address{ + FormattedAddress: r.Results[0].Formatted, + HouseNumber: addr.HouseNumber, + Street: addr.Street(), + Suburb: addr.Suburb, + Postcode: addr.Postcode, + City: addr.Locality(), + CountryCode: strings.ToUpper(addr.CountryCode), + Country: addr.Country, + County: addr.County, + State: addr.State, + StateDistrict: addr.StateDistrict, + }, nil } diff --git a/opencage/geocoder_test.go b/opencage/geocoder_test.go index 8377e54..41674fb 100644 --- a/opencage/geocoder_test.go +++ b/opencage/geocoder_test.go @@ -1,14 +1,14 @@ package opencage_test import ( - "github.com/codingsince1985/geo-golang" - "github.com/codingsince1985/geo-golang/opencage" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "os" "strings" "testing" + + "github.com/codingsince1985/geo-golang/opencage" + "github.com/stretchr/testify/assert" ) var key = os.Getenv("OPENCAGE_API_KEY") @@ -23,7 +23,7 @@ func TestGeocode(t *testing.T) { geocoder := opencage.Geocoder(key, ts.URL+"/") location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") - assert.NoError(t, err) + assert.Nil(t, err) assert.InDelta(t, -37.8154176, location.Lat, locDelta) assert.InDelta(t, 144.9665563, location.Lng, locDelta) } @@ -35,7 +35,7 @@ func TestReverseGeocode(t *testing.T) { geocoder := opencage.Geocoder(key, ts.URL+"/") address, err := geocoder.ReverseGeocode(-37.8154176, 144.9665563) assert.NoError(t, err) - assert.True(t, strings.Index(address, "Collins St") > 0) + assert.True(t, strings.Index(address.FormattedAddress, "Collins St") > 0) } func TestReverseGeocodeWithNoResult(t *testing.T) { @@ -43,8 +43,10 @@ func TestReverseGeocodeWithNoResult(t *testing.T) { defer ts.Close() geocoder := opencage.Geocoder(key, ts.URL+"/") - _, err := geocoder.ReverseGeocode(-37.8154176, 164.9665563) - assert.Equal(t, err, geo.ErrNoResult) + //geocoder := opencage.Geocoder(key) + address, err := geocoder.ReverseGeocode(-37.8154176, 164.9665563) + assert.Nil(t, err) + assert.Nil(t, address) } func testServer(response string) *httptest.Server { diff --git a/openstreetmap/geocoder.go b/openstreetmap/geocoder.go index 4e7fdca..47c443c 100644 --- a/openstreetmap/geocoder.go +++ b/openstreetmap/geocoder.go @@ -3,14 +3,20 @@ package openstreetmap import ( "fmt" + "strings" + "github.com/codingsince1985/geo-golang" + "github.com/codingsince1985/geo-golang/osm" ) type ( baseURL string geocodeResponse struct { - DisplayName string `json:"display_name"` - Lat, Lon, Error string + DisplayName string `json:"display_name"` + Lat string + Lon string + Error string + Addr osm.Address `json:"address"` } ) @@ -33,16 +39,34 @@ func (b baseURL) ReverseGeocodeURL(l geo.Location) string { return string(b) + "reverse?" + fmt.Sprintf("format=json&lat=%f&lon=%f", l.Lat, l.Lng) } -func (r *geocodeResponse) Location() geo.Location { - if r.Error == "" { - return geo.Location{geo.ParseFloat(r.Lat), geo.ParseFloat(r.Lon)} +func (r *geocodeResponse) Location() (*geo.Location, error) { + if r.Error != "" { + return nil, fmt.Errorf("geocoding error: %s", r.Error) + } + if r.Lat == "" && r.Lon == "" { + return nil, nil } - return geo.Location{} + + return &geo.Location{ + Lat: geo.ParseFloat(r.Lat), + Lng: geo.ParseFloat(r.Lon), + }, nil } -func (r *geocodeResponse) Address() string { - if r.Error == "" { - return r.DisplayName +func (r *geocodeResponse) Address() (*geo.Address, error) { + if r.Error != "" { + return nil, fmt.Errorf("reverse geocoding error: %s", r.Error) } - return "" + + return &geo.Address{ + FormattedAddress: r.DisplayName, + HouseNumber: r.Addr.HouseNumber, + Street: r.Addr.Street(), + Postcode: r.Addr.Postcode, + City: r.Addr.Locality(), + Suburb: r.Addr.Suburb, + State: r.Addr.State, + Country: r.Addr.Country, + CountryCode: strings.ToUpper(r.Addr.CountryCode), + }, nil } diff --git a/openstreetmap/geocoder_test.go b/openstreetmap/geocoder_test.go index d7e00dc..42b420a 100644 --- a/openstreetmap/geocoder_test.go +++ b/openstreetmap/geocoder_test.go @@ -17,8 +17,8 @@ func TestGeocode(t *testing.T) { geocoder := openstreetmap.GeocoderWithURL(ts.URL + "/") location, err := geocoder.Geocode("60 Collins St, Melbourne VIC 3000") - assert.NoError(t, err) - assert.Equal(t, geo.Location{Lat: -37.8157915, Lng: 144.9656171}, location) + assert.Nil(t, err) + assert.Equal(t, geo.Location{Lat: -37.8157915, Lng: 144.9656171}, *location) } func TestReverseGeocode(t *testing.T) { @@ -27,8 +27,8 @@ func TestReverseGeocode(t *testing.T) { geocoder := openstreetmap.GeocoderWithURL(ts.URL + "/") address, err := geocoder.ReverseGeocode(-37.8157915, 144.9656171) - assert.NoError(t, err) - assert.True(t, strings.Index(address, "Collins St") > 0) + assert.Nil(t, err) + assert.True(t, strings.Index(address.FormattedAddress, "Collins St") > 0) } func TestReverseGeocodeWithNoResult(t *testing.T) { @@ -36,8 +36,10 @@ func TestReverseGeocodeWithNoResult(t *testing.T) { defer ts.Close() geocoder := openstreetmap.GeocoderWithURL(ts.URL + "/") - _, err := geocoder.ReverseGeocode(-37.8157915, 164.9656171) - assert.Equal(t, err, geo.ErrNoResult) + //geocoder := openstreetmap.Geocoder() + addr, err := geocoder.ReverseGeocode(-37.8157915, 164.9656171) + assert.Nil(t, addr) + assert.NotNil(t, err) } func testServer(response string) *httptest.Server { diff --git a/osm/osm.go b/osm/osm.go new file mode 100644 index 0000000..aba8533 --- /dev/null +++ b/osm/osm.go @@ -0,0 +1,60 @@ +// Package osm provides common types for OpenStreetMap used by various providers +// and some helper functions to reduce code repetition across specific client implementations. +package osm + +// Address contains address fields specific to OpenStreetMap +type Address struct { + HouseNumber string `json:"house_number"` + Road string `json:"road"` + Pedestrian string `json:"pedestrian"` + Footway string `json:"footway"` + Cycleway string `json:"cycleway"` + Highway string `json:"highway"` + Path string `json:"path"` + Suburb string `json:"suburb"` + City string `json:"city"` + Town string `json:"town"` + Village string `json:"village"` + County string `json:"county"` + Country string `json:"country"` + CountryCode string `json:"country_code"` + State string `json:"state"` + StateDistrict string `json:"state_district"` + Postcode string `json:"postcode"` +} + +// Locality checks different fields for the locality name +func (a Address) Locality() string { + var locality string + + if a.City != "" { + locality = a.City + } else if a.Town != "" { + locality = a.Town + } else if a.Village != "" { + locality = a.Village + } + + return locality +} + +// Street checks different fields for the street name +func (a Address) Street() string { + var street string + + if a.Road != "" { + street = a.Road + } else if a.Pedestrian != "" { + street = a.Pedestrian + } else if a.Path != "" { + street = a.Path + } else if a.Cycleway != "" { + street = a.Cycleway + } else if a.Footway != "" { + street = a.Footway + } else if a.Highway != "" { + street = a.Highway + } + + return street +}