├── geocoder_test.go ├── MIT_LICENSE ├── geocoder.go ├── README.md ├── geocoding_test.go ├── geocoding.go ├── directions_test.go └── directions.go /geocoder_test.go: -------------------------------------------------------------------------------- 1 | package geocoder 2 | 3 | import "testing" 4 | 5 | func TestSetAPIKey(t *testing.T) { 6 | key := apiKey 7 | SetAPIKey("foo") 8 | if apiKey != "foo" { 9 | t.Errorf("Expected foo ~ Received %s", apiKey) 10 | } 11 | SetAPIKey(key) 12 | } 13 | -------------------------------------------------------------------------------- /MIT_LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Jason Winn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /geocoder.go: -------------------------------------------------------------------------------- 1 | // Golang mapquest api 2 | 3 | package geocoder 4 | 5 | import ( 6 | "encoding/json" 7 | "net/http" 8 | ) 9 | 10 | var apiKey = "Fmjtd%7Cluub256alu%2C7s%3Do5-9u82ur" 11 | 12 | // SetAPIKey lets you set your own api key. 13 | // The default api key is probably okay to use for testing. 14 | // But for production, you should create your own key at http://mapquestapi.com 15 | func SetAPIKey(key string) { 16 | apiKey = key 17 | } 18 | 19 | // Shortcut for creating a json decoder out of a response 20 | func decoder(resp *http.Response) *json.Decoder { 21 | return json.NewDecoder(resp.Body) 22 | } 23 | 24 | // LatLng specifies a point with latitude and longitude 25 | type LatLng struct { 26 | Lat float64 `json:"lat"` 27 | Lng float64 `json:"lng"` 28 | } 29 | 30 | // Location is specified by its address and coordinates 31 | type Location struct { 32 | Street string `json:"street"` 33 | City string `json:"adminArea5"` 34 | State string `json:"adminArea3"` 35 | PostalCode string `json:"postalCode"` 36 | County string `json:"adminArea4"` 37 | CountryCode string `json:"adminArea1"` 38 | LatLng LatLng `json:"latLng"` 39 | Type string `json:"type"` 40 | DragPoint bool `json:"dragPoint"` 41 | } 42 | 43 | // Complete geocoding result 44 | type GeocodingResult struct { 45 | Info Info `json:"info"` 46 | Options struct { 47 | MaxResults int `json:"maxResults"` 48 | ThumbMaps bool `json:"thumbMaps"` 49 | IgnoreLatLngInput bool `json:"ignoreLatLngInput"` 50 | } `json:"options"` 51 | Results []struct { 52 | ProvidedLocation struct { 53 | Location string `json:"location"` 54 | } `json:"providedLocation"` 55 | Locations []struct { 56 | Street string `json:"street"` 57 | // Neighborhood 58 | AdminArea6 string `json:"adminArea6"` 59 | AdminArea6Type string `json:"adminArea6Type"` 60 | // City 61 | AdminArea5 string `json:"adminArea5"` 62 | AdminArea5Type string `json:"adminArea5Type"` 63 | // County 64 | AdminArea4 string `json:"adminArea4"` 65 | AdminArea4Type string `json:"adminArea4Type"` 66 | // State 67 | AdminArea3 string `json:"adminArea3"` 68 | AdminArea3Type string `json:"adminArea3Type"` 69 | // Country 70 | AdminArea1 string `json:"adminArea1"` 71 | AdminArea1Type string `json:"adminArea1Type"` 72 | PostalCode string `json:"postalCode"` 73 | GeocodeQualityCode string `json:"geocodeQualityCode"` 74 | // ex: "NEIGHBORHOOD", "CITY", "COUNTY" 75 | GeocodeQuality string `json:"geocodeQuality"` 76 | DragPoint bool `json:"dragPoint"` 77 | SideOfStreet string `json:"sideOfStreet"` 78 | LinkId string `json:"linkId"` 79 | UnknownInput string `json:"unknownInput"` 80 | Type string `json:"type"` 81 | LatLng LatLng `json:"latLng"` 82 | DisplayLatLng LatLng `json:"displayLatLng"` 83 | MapUrl string `json:"mapUrl"` 84 | } `json:"locations"` 85 | } `json:"results"` 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Mapquest geocoder and directions for Go (golang) 2 | ================================================ 3 | 4 | ## What it does 5 | 6 | * Returns a Longitude and Latitude for a given string query 7 | * Returns an address for a Longitude and Longitude 8 | * Returns directions between two or more points. (JSON or XML) 9 | 10 | ## API Key 11 | Get a free API Key at [http://mapquestapi.com](http://mapquestapi.com). 12 | 13 | ## Why MapQuest API? 14 | Google Maps Geocoding API has a limitation that prohibits querying their 15 | geocoding API unless you will be displaying the results on a Google Map. 16 | Google directions is limited to 2 requests per second. 17 | MapQuest's geocoding API does not have these restrictions. 18 | 19 | ## Installation 20 | 21 | * go get "github.com/jasonwinn/geocoder" 22 | * import "github.com/jasonwinn/geocoder" 23 | 24 | 25 | ## Examples 26 | 27 | ### Set API Key 28 | 29 | You'll want to set an api key for the Mapquest API to go into production. 30 | ```go 31 | // this is the testing key used in `go test` 32 | SetAPIKey("Fmjtd%7Cluub256alu%2C7s%3Do5-9u82ur") 33 | ``` 34 | 35 | ### Geocode 36 | 37 | To retrieve just the latitude and longitude of the best match 38 | for a particular query, use the Geocode method: 39 | 40 | ```go 41 | query := "Seattle WA" 42 | lat, lng, err := geocoder.Geocode(query) 43 | if err != nil { 44 | panic("THERE WAS SOME ERROR!!!!!") 45 | } 46 | 47 | // 47.6064, -122.330803 48 | 49 | ``` 50 | 51 | To retrieve a full geocoding result including all matches as well as 52 | additional location information per match like the street, city and state, 53 | use the FullGeocode method: 54 | 55 | 56 | ```go 57 | query := "Seattle WA" 58 | result, err := geocoder.FullGeocode(query) 59 | if err != nil { 60 | panic("THERE WAS SOME ERROR!!!!!") 61 | } 62 | 63 | // GeocodingResult instance 64 | 65 | ``` 66 | 67 | ### Reverse Geocode 68 | ```go 69 | address, err := geocoder.ReverseGeocode(47.6064, -122.330803) 70 | if err != nil { 71 | panic("THERE WAS SOME ERROR!!!!!") 72 | } 73 | 74 | address.Street // 542 Marion St 75 | address.City // Seattle 76 | address.State // WA 77 | address.PostalCode // 98104 78 | address.County // King 79 | address.CountryCode // US 80 | 81 | ``` 82 | 83 | ### Directions 84 | ```go 85 | directions := NewDirections("Amsterdam,Netherlands", []string{"Antwerp,Belgium"}) 86 | results, err := directions.Get() 87 | if err != nil { 88 | panic("THERE WAS SOME ERROR!!!!!") 89 | } 90 | 91 | route := results.Route 92 | time := route.Time 93 | legs := route.Legs 94 | distance := route.Distance 95 | 96 | // or get distance with this shortcut 97 | // 98 | // use "k" to return result in km 99 | // use "m" to return result in miles 100 | distance, err := directions.Distance("k") 101 | if err != nil { 102 | panic("THERE WAS SOME ERROR!!!!!") 103 | } 104 | ``` 105 | 106 | ## Documentation 107 | 108 | [https://godoc.org/github.com/jasonwinn/geocoder](https://godoc.org/github.com/jasonwinn/geocoder) 109 | 110 | -------------------------------------------------------------------------------- /geocoding_test.go: -------------------------------------------------------------------------------- 1 | package geocoder 2 | 3 | import "testing" 4 | 5 | const ( 6 | city = "Seattle" 7 | state = "WA" 8 | postalCode = "98164" 9 | seattleLat = 47.603832 10 | seattleLng = -122.330062 11 | antwerpLat = 51.221110 12 | antwerpLng = 4.399708 13 | beijingLat = 39.905963 14 | beijingLng = 116.391248 15 | ) 16 | 17 | func TestGeocode(t *testing.T) { 18 | query := "Seattle WA" 19 | lat, lng, err := Geocode(query) 20 | if err != nil { 21 | t.Errorf("Seattle: Expected error to be nil ~ Received %v", err) 22 | } 23 | 24 | if lat != seattleLat || lng != seattleLng { 25 | t.Errorf("Seattle: Expected (%f, %f) ~ Received (%f, %f)", seattleLat, seattleLng, lat, lng) 26 | } 27 | } 28 | 29 | func TestReverseGeoCode(t *testing.T) { 30 | address, err := ReverseGeocode(seattleLat, seattleLng) 31 | if err != nil { 32 | t.Errorf("Seattle (reverse): Expected error to be nil ~ Received %v", err) 33 | } 34 | 35 | if address != nil && address.City != city || address.State != state || address.PostalCode != postalCode { 36 | t.Errorf("Seattle (reverse): Expected %s %s %s ~ Received %s %s %s", 37 | city, state, postalCode, address.City, address.State, address.PostalCode) 38 | } 39 | } 40 | 41 | func TestBatchGeocode(t *testing.T) { 42 | latLngs, err := BatchGeocode([]string{"Antwerp,Belgium", "Beijing,China"}) 43 | if err != nil { 44 | t.Errorf("Seattle (reverse): Expected error to be nil ~ Received %v", err) 45 | } 46 | 47 | if len(latLngs) != 2 { 48 | t.Fatalf("Batch: Expected len(batch) to be 2, got %v", latLngs) 49 | } 50 | antwerp := latLngs[0] 51 | if antwerp.Lat != antwerpLat || antwerp.Lng != antwerpLng { 52 | t.Errorf("Antwerp: Expected %f, %f ~ Received %f, %f", antwerpLat, antwerpLng, antwerp.Lat, antwerp.Lng) 53 | } 54 | beijing := latLngs[1] 55 | if beijing.Lat != beijingLat || beijing.Lng != beijingLng { 56 | t.Errorf("Beijng: Expected %f, %f ~ Received %f, %f", beijingLat, beijingLng, beijing.Lat, beijing.Lng) 57 | } 58 | } 59 | 60 | func TestGeocodeShouldFail(t *testing.T) { 61 | query := "Seattle WA" 62 | // set a bad api key 63 | savedApiKey := apiKey 64 | SetAPIKey("bad api key that doesn't exist, hopefully!") 65 | lat, lng, err := Geocode(query) 66 | SetAPIKey(savedApiKey) 67 | 68 | if err == nil { 69 | t.Errorf("Seattle: Expected error to not be nil ~ Received %v", err) 70 | } 71 | 72 | if lat != 0 || lng != 0 { 73 | t.Errorf("Seattle: Expected (0, 0) ~ Received (%f, %f)", lat, lng) 74 | } 75 | } 76 | 77 | func TestReverseGeoCodeShouldFail(t *testing.T) { 78 | // set a bad api key 79 | savedApiKey := apiKey 80 | SetAPIKey("bad api key that doesn't exist, hopefully!") 81 | address, err := ReverseGeocode(seattleLat, seattleLng) 82 | SetAPIKey(savedApiKey) 83 | 84 | if err == nil { 85 | t.Errorf("Seattle (reverse): Expected error to not be nil ~ Received %v", err) 86 | } 87 | 88 | if address != nil { 89 | t.Errorf("Seattle (reverse): Expected address to be nil ~ Received %v", 90 | address) 91 | } 92 | } 93 | 94 | func TestFullGeocode(t *testing.T) { 95 | query := "Seattle WA" 96 | result, err := FullGeocode(query) 97 | if err != nil { 98 | t.Errorf("Seattle: Expected error to be nil ~ Received %v", err) 99 | } 100 | 101 | location := result.Results[0].Locations[0] 102 | if location.LatLng.Lat != seattleLat || location.LatLng.Lng != seattleLng { 103 | t.Errorf("Seattle: Expected (%f, %f) ~ Received (%f, %f)", seattleLat, seattleLng, location.LatLng.Lat, location.LatLng.Lng) 104 | } 105 | } 106 | 107 | func TestBatchGeocodeShouldFail(t *testing.T) { 108 | // set a bad api key 109 | savedApiKey := apiKey 110 | SetAPIKey("bad api key that doesn't exist, hopefully!") 111 | latLngs, err := BatchGeocode([]string{"Antwerp,Belgium", "Beijing,China"}) 112 | SetAPIKey(savedApiKey) 113 | 114 | if err == nil { 115 | t.Errorf("Batch: Expected error to be nil ~ Received %v", err) 116 | } 117 | 118 | if len(latLngs) != 0 { 119 | t.Errorf("Batch: Expected len(batch) to be 0, got %v", latLngs) 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /geocoding.go: -------------------------------------------------------------------------------- 1 | /* Exposes (partially) the mapquest geocoding api. 2 | 3 | Reference: http://open.mapquestapi.com/geocoding/ 4 | 5 | Example: 6 | 7 | lat, lng := Geocode("Seattle WA") 8 | location, err := ReverseGeocode(47.603561, -122.329437) 9 | 10 | */ 11 | 12 | package geocoder 13 | 14 | import ( 15 | "bytes" 16 | "encoding/json" 17 | "fmt" 18 | "net/http" 19 | "net/url" 20 | ) 21 | 22 | const ( 23 | geocodeURL = "https://open.mapquestapi.com/geocoding/v1/address?inFormat=kvp&outFormat=json&location=" 24 | reverseGeocodeURL = "https://open.mapquestapi.com/geocoding/v1/reverse?location=" 25 | batchGeocodeURL = "https://open.mapquestapi.com/geocoding/v1/batch?key=" 26 | ) 27 | 28 | // Returns the latitude and longitude of the best location match 29 | // for the specified query. 30 | func Geocode(address string) (float64, float64, error) { 31 | // Query Provider 32 | resp, err := http.Get(geocodeURL + url.QueryEscape(address) + "&key=" + apiKey) 33 | 34 | if err != nil { 35 | return 0, 0, fmt.Errorf("Error geocoding address: <%v>", err) 36 | } 37 | 38 | defer resp.Body.Close() 39 | 40 | // Decode our JSON results 41 | var result geocodingResults 42 | err = decoder(resp).Decode(&result) 43 | 44 | if err != nil { 45 | return 0, 0, fmt.Errorf("Error decoding geocoding result: <%v>", err) 46 | } 47 | 48 | var lat float64 49 | var lng float64 50 | if len(result.Results[0].Locations) > 0 { 51 | lat = result.Results[0].Locations[0].LatLng.Lat 52 | lng = result.Results[0].Locations[0].LatLng.Lng 53 | } 54 | 55 | return lat, lng, nil 56 | } 57 | 58 | // Returns the full geocoding response including all of the matches 59 | // as well as reverse-geocoded for each match location. 60 | func FullGeocode(address string) (*GeocodingResult, error) { 61 | // Query Provider 62 | resp, err := http.Get(geocodeURL + url.QueryEscape(address) + "&key=" + apiKey) 63 | 64 | if err != nil { 65 | return nil, fmt.Errorf("Error geocoding address: <%v>", err) 66 | } 67 | 68 | defer resp.Body.Close() 69 | 70 | // Decode our JSON results 71 | var result GeocodingResult 72 | err = decoder(resp).Decode(&result) 73 | 74 | if err != nil { 75 | return nil, fmt.Errorf("Error decoding geocoding result: <%v>", err) 76 | } 77 | 78 | return &result, nil 79 | } 80 | 81 | // Returns the address for a latitude and longitude. 82 | func ReverseGeocode(lat float64, lng float64) (*Location, error) { 83 | // Query Provider 84 | resp, err := http.Get(reverseGeocodeURL + 85 | fmt.Sprintf("%f,%f&key=%s", lat, lng, apiKey)) 86 | 87 | if err != nil { 88 | return nil, fmt.Errorf("Error reverse geocoding lat, long pair: <%v>", err) 89 | } 90 | 91 | defer resp.Body.Close() 92 | 93 | // Decode our JSON results 94 | var result geocodingResults 95 | err = decoder(resp).Decode(&result) 96 | 97 | if err != nil { 98 | return nil, fmt.Errorf("Error decoding reverse geocoding result: <%v>", err) 99 | } 100 | 101 | var location Location 102 | 103 | // Assign the results to the Location struct 104 | if len(result.Results[0].Locations) > 0 { 105 | location = result.Results[0].Locations[0] 106 | } 107 | 108 | return &location, nil 109 | } 110 | 111 | // Geocodes multiple locations with a single API request. 112 | // Up to 100 locations per call may be provided. 113 | func BatchGeocode(addresses []string) ([]LatLng, error) { 114 | var next, start, end int 115 | n := len(addresses) 116 | latLngs := make([]LatLng, n) 117 | batches := n/100 + 1 118 | next = 0 119 | for batch := 0; batch < batches; batch++ { 120 | start = next 121 | next = (batch + 1) * 100 122 | if n < next { 123 | end = n 124 | } else { 125 | end = next 126 | } 127 | bgb := batchGeocodeBody{ 128 | Locations: addresses[start:end], 129 | MaxResults: 1, 130 | ThumbMaps: false, 131 | } 132 | b, err := json.Marshal(bgb) 133 | if err != nil { 134 | return nil, err 135 | } 136 | body := bytes.NewBuffer(b) 137 | resp, err := http.Post(batchGeocodeURL+apiKey, "application/json", body) 138 | if err != nil { 139 | return nil, err 140 | } 141 | defer resp.Body.Close() 142 | var result geocodingResults 143 | err = decoder(resp).Decode(&result) 144 | if err != nil { 145 | return nil, err 146 | } 147 | for i, r := range result.Results { 148 | if len(r.Locations) == 0 { 149 | latLngs[start+i] = LatLng{Lat: 0, Lng: 0} 150 | } else { 151 | latLngs[start+i] = r.Locations[0].LatLng 152 | } 153 | } 154 | } 155 | return latLngs, nil 156 | } 157 | 158 | // geocodingResults contains the locations of a geocoding request 159 | // MapQuest providers more JSON fields than this but this is all we are interested in. 160 | type geocodingResults struct { 161 | Results []struct { 162 | Locations []Location `json:"locations"` 163 | } `json:"results"` 164 | } 165 | 166 | // batchGeocodeBody will be marshalled as json to send in body with http post 167 | type batchGeocodeBody struct { 168 | Locations []string `json:"locations"` 169 | MaxResults int 170 | ThumbMaps bool `json:"thumbMaps"` 171 | } 172 | -------------------------------------------------------------------------------- /directions_test.go: -------------------------------------------------------------------------------- 1 | package geocoder 2 | 3 | import ( 4 | "fmt" 5 | "math" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | const ( 11 | templDump = "{\"route\":{\"hasTollRoad\":false,\"hasBridge\":false,\"distance\":0.27,\"shape\":{\"legIndexes\":[],\"maneuverIndexes\":[],\"shapePoints\":[51.529315,-0.269962,51.529087,-0.271016,51.52901,-0.271373,51.52885,-0.272208,51.528568,-0.274354,51.528472,-0.275138,51.528324,-0.27602]},\"hasTunnel\":false,\"hasHighway\":false,\"computedWaypoints\":[],\"routeError\":{\"errorCode\":-400,\"message\":\"\"},\"formattedTime\":\"00:00:41\",\"sessionId\":\"%s\",\"realTime\":-1,\"hasSeasonalClosure\":false,\"hasCountryCross\":false,\"fuelUsed\":0.02,\"legs\":[{\"hasTollRoad\":false,\"hasBridge\":false,\"destNarrative\":\"\",\"distance\":0.27,\"hasTunnel\":false,\"hasHighway\":false,\"index\":0,\"formattedTime\":\"00:00:41\",\"origIndex\":-1,\"hasSeasonalClosure\":false,\"hasCountryCross\":false,\"roadGradeStrategy\":[],\"destIndex\":-1,\"time\":41,\"hasUnpaved\":false,\"origNarrative\":\"\",\"maneuvers\":[{\"distance\":0.27,\"streets\":[\"Coronation Road\"],\"narrative\":\"\",\"turnType\":0,\"startPoint\":{\"lng\":-0.269962,\"lat\":51.529315},\"index\":0,\"formattedTime\":\"00:00:41\",\"directionName\":\"West\",\"maneuverNotes\":[],\"linkIds\":[91225298,91225281,91222079,91222078,91222062],\"signs\":[],\"" 12 | testDistance = 157 13 | testLegs = 1 14 | testManeuvers = 52 15 | testStatuscode = 0 16 | testTime = 6085 17 | testUnit = "m" 18 | testURL = "https://open.mapquestapi.com/directions/v2/route?inFormat=kvp&key=Fmjtd%7Cluub256alu%2C7s%3Do5-9u82ur&outFormat=json&from=Amsterdam%2CNetherlands&to=Antwerp%2CBelgium&unit=m&routeTypefastest&narrativeType=text&enhancedNarrative=false&maxLinkId=0&locale=en_US&avoids=Ferry&mustAvoidLinkIds=5,7&stateBoundaryDisplay=true&countryBoundaryDisplay=true&destinationManeuverDisplay=true&fullShape=false&cyclingRoadFactor=1&roadGradeStrategy=DEFAULT_STRATEGY&drivingStyle=normal&highwayEfficiency=22&manMaps=true&walkingSpeed=-1" 19 | ) 20 | 21 | func unexpected(err error, t *testing.T) bool { 22 | if err != nil { 23 | t.Errorf("Unexpected error: %v", err) 24 | return true 25 | } 26 | return false 27 | } 28 | 29 | func TestUrl(t *testing.T) { 30 | directions := NewDirections("Amsterdam,Netherlands", []string{"Antwerp,Belgium"}) 31 | directions.MustAvoidLinkIDs = []int{5, 7} 32 | directions.Avoids = []string{"Ferry"} 33 | routeURL := directions.URL("json") 34 | if testURL != routeURL { 35 | t.Errorf("Expected %s ~ Received %s", testURL, routeURL) 36 | } 37 | } 38 | 39 | func TestDistance(t *testing.T) { 40 | directions := NewDirections("Amsterdam,Netherlands", []string{"Antwerp,Belgium"}) 41 | distance, err := directions.Distance("k") 42 | if unexpected(err, t) { 43 | return 44 | } 45 | if testDistance != int(distance) { 46 | t.Errorf("Expected %d ~ Received %d", testDistance, int(distance)) 47 | } 48 | // distance function may not alter the original unit 49 | if directions.Unit != "m" { 50 | t.Errorf("Unit: Expected %s ~ Received %s", testUnit, directions.Unit) 51 | } 52 | } 53 | 54 | func TestDistanceError(t *testing.T) { 55 | directions := NewDirections("Amsterdam,Netherlands", []string{"sdfa"}) 56 | _, err := directions.Distance("k") 57 | if err.Error() != "Error 402: We are unable to route with the given locations." { 58 | t.Errorf("Expected %s ~ Received %s", "", err.Error()) 59 | } 60 | } 61 | 62 | func TestDirections(t *testing.T) { 63 | directions := NewDirections("Amsterdam,Netherlands", []string{"Antwerp,Belgium"}) 64 | directions.Unit = "k" // switch to km 65 | directions.ManMaps = false 66 | //fmt.Println(string(directions.Dump("json"))) 67 | results, err := directions.Get() 68 | if unexpected(err, t) { 69 | return 70 | } 71 | route := results.Route 72 | if route.HasTollRoad { 73 | t.Errorf("Route.HasTollRoad: Expected false ~ Received %v", route.HasUnpaved) 74 | } 75 | if route.HasUnpaved { 76 | t.Errorf("Route.HasUnpaved: Expected false ~ Received %v", route.HasUnpaved) 77 | } 78 | if !route.HasHighway { 79 | t.Errorf("Route.HasHighway: Expected true ~ Received %v", route.HasHighway) 80 | } 81 | distance := int(route.Distance) 82 | if testDistance != distance { 83 | t.Errorf("Route.Distance: Expected %d ~ Received %d", testDistance, distance) 84 | } 85 | if math.Abs(float64(testTime-route.Time)) > 60.0 { // tolerate minute difference 86 | t.Errorf("Route.Time: Expected %d ~ Received %d", testTime, route.Time) 87 | } 88 | legs := route.Legs 89 | if testLegs != len(legs) { 90 | t.Errorf("len(Route.Legs): Expected %d ~ Received %d", testLegs, len(legs)) 91 | } else if testManeuvers != len(legs[0].Maneuvers) { 92 | t.Errorf("Route.Legs[0].Maneuvers: Expected %d ~ Received %d", testManeuvers, len(legs[0].Maneuvers)) 93 | } 94 | statuscode := results.Info.Statuscode 95 | if testStatuscode != statuscode { 96 | t.Errorf("Info.Statuscode: Expected %d ~ Received %d", testStatuscode, statuscode) 97 | } 98 | } 99 | 100 | func TestDump(t *testing.T) { 101 | directions := NewDirections("1 Coronation Road, London, United Kingdom", []string{"3 Coronation Road, London, United Kingdom"}) 102 | directions.NarrativeType = "none" 103 | directions.ManMaps = false 104 | results, err := directions.Get() 105 | if unexpected(err, t) { 106 | return 107 | } 108 | sessionID := results.Route.SessionID 109 | directions.SessionID = sessionID 110 | testDump := fmt.Sprintf(templDump, sessionID) 111 | dumpBytes, err := directions.Dump("json") 112 | if unexpected(err, t) { 113 | return 114 | } 115 | // avoid mapUrl as it contains a random sequence which is always different 116 | dump := strings.SplitN(string(dumpBytes), "mapUrl", 2)[0] 117 | if testDump != dump { 118 | t.Errorf("Expected\n%s\nReceived\n%s", testDump, dump) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /directions.go: -------------------------------------------------------------------------------- 1 | /* Exposes (partially) the mapquest directions api. 2 | 3 | Reference: http://open.mapquestapi.com/directions/ 4 | 5 | Example: 6 | 7 | directions := NewDirections("Amsterdam,Netherlands", []string{"Antwerp,Belgium"}) 8 | directions.Unit = "k" // switch to km 9 | 10 | url := directions.URL("json") 11 | json := directions.Dump("json") 12 | xml := directions.Dump("xml") 13 | distance := directions.Distance("k") 14 | route := directions.Get().Route 15 | 16 | */ 17 | 18 | package geocoder 19 | 20 | import ( 21 | "bytes" 22 | "fmt" 23 | "io/ioutil" 24 | "net/http" 25 | "net/url" 26 | "strconv" 27 | "strings" 28 | ) 29 | 30 | const ( 31 | directionURL = "https://open.mapquestapi.com/directions/v2/route?inFormat=kvp&key=" 32 | ) 33 | 34 | // Directions provide information on how to get from one location 35 | // to one (or more) other locations together with copyright and statuscode info. 36 | // (style, shape and mapstate options are not implemented) 37 | type Directions struct { 38 | // Starting location 39 | From string 40 | // Ending location 41 | To []string 42 | // type of units for calculating distance: m (Miles, default) or k (Km) 43 | Unit string 44 | // fastest(default), shortest, pedestrian, multimodal, bicycle 45 | RouteType string 46 | // If true(default), reverse geocode call will be performed even on lat/long. 47 | DoReverseGeocode bool 48 | // none, text(default), html, microformat 49 | NarrativeType string 50 | // Encompass extra advice such as intersections (default false) 51 | EnhancedNarrative bool 52 | // The maximum number of Link IDs to return for each maneuver (default is 0) 53 | MaxLinkID int 54 | // en_US(default), en_GB, fr_CA, fr_FR, de_DE, es_ES, es_MX, ru_RU 55 | Locale string 56 | // Limited Access, Toll Road, Ferry, Unpaved, Seasonal Closure, Country Crossing 57 | Avoids []string 58 | // Link IDs of roads to absolutely avoid. May cause some routes to fail. 59 | MustAvoidLinkIDs []int 60 | // Link IDs of roads to try to avoid during route calculation without guarantee. 61 | TryAvoidLinkIDs []int 62 | // Whether state boundary crossings will be displayed in narrative. (default true) 63 | StateBoundaryDisplay bool 64 | // Whether country boundary crossings will be displayed in narrative. (default true) 65 | CountryBoundaryDisplay bool 66 | // Whether "End at" destination maneuver will be displayed in narrative. (default true) 67 | DestinationManeuverDisplay bool 68 | // To return a route shape without a mapState. (default false) 69 | FullShape bool 70 | // A value of < 1 favors cycling on non-bike lane roads. [0.1..1(default)..100] 71 | CyclingRoadFactor float64 72 | // DEFAULT_STRATEGY (default), AVOID_UP_HILL, AVOID_DOWN_HILL,AVOID_ALL_HILLS,FAVOR_UP_HILL,FAVOR_DOWN_HILL,FAVOR_ALL_HILLS 73 | RoadGradeStrategy string 74 | // cautious, normal, aggressive 75 | DrivingStyle string 76 | // Fuel efficiency, given as miles per gallon. (0..235 mpg) 77 | HighwayEfficiency float64 78 | // If true (default), a small staticmap is displayed per maneuver 79 | ManMaps bool 80 | // Walking speed, always in miles per hour independent from unit (default 2.5) 81 | WalkingSpeed float64 82 | // Session id 83 | SessionID string 84 | } 85 | 86 | // NewDirections is a constructor to initialize a Directions struct 87 | // with mapquest defaults. 88 | func NewDirections(from string, to []string) *Directions { 89 | return &Directions{ 90 | From: from, 91 | To: to, 92 | Unit: "m", 93 | RouteType: "fastest", 94 | DoReverseGeocode: true, 95 | NarrativeType: "text", 96 | EnhancedNarrative: false, 97 | MaxLinkID: 0, 98 | Locale: "en_US", 99 | StateBoundaryDisplay: true, 100 | CountryBoundaryDisplay: true, 101 | DestinationManeuverDisplay: true, 102 | FullShape: false, 103 | CyclingRoadFactor: 1, 104 | RoadGradeStrategy: "DEFAULT_STRATEGY", 105 | DrivingStyle: "normal", 106 | HighwayEfficiency: 22, 107 | ManMaps: true, 108 | WalkingSpeed: -1, // 2.5 109 | SessionID: "", 110 | } 111 | } 112 | 113 | // URL constructs the mapquest directions url (format is json or xml) 114 | func (directions Directions) URL(format string) string { 115 | // http://stackoverflow.com/questions/1760757/how-to-efficiently-concatenate-strings-in-go 116 | var ( 117 | n int 118 | routeURL bytes.Buffer 119 | ) 120 | writeStringInts := func(label string, arrayInts []int) { 121 | n = len(arrayInts) 122 | if n > 0 { 123 | routeURL.WriteString(label) 124 | for i, number := range arrayInts { 125 | routeURL.WriteString(strconv.Itoa(number)) 126 | if i < n-1 { 127 | routeURL.WriteString(",") 128 | } 129 | } 130 | } 131 | } 132 | routeURL.WriteString(directionURL) 133 | routeURL.WriteString(apiKey) 134 | routeURL.WriteString("&outFormat=" + format) 135 | routeURL.WriteString("&from=" + url.QueryEscape(directions.From)) 136 | for _, to := range directions.To { 137 | routeURL.WriteString("&to=" + url.QueryEscape(to)) 138 | } 139 | routeURL.WriteString("&unit=" + directions.Unit) 140 | routeURL.WriteString("&routeType" + directions.RouteType) 141 | routeURL.WriteString("&narrativeType=" + directions.NarrativeType) 142 | routeURL.WriteString("&enhancedNarrative=" + strconv.FormatBool(directions.EnhancedNarrative)) 143 | routeURL.WriteString("&maxLinkId=" + strconv.Itoa(directions.MaxLinkID)) 144 | routeURL.WriteString("&locale=" + directions.Locale) 145 | for _, avoids := range directions.Avoids { 146 | routeURL.WriteString("&avoids=" + url.QueryEscape(avoids)) 147 | } 148 | writeStringInts("&mustAvoidLinkIds=", directions.MustAvoidLinkIDs) 149 | writeStringInts("&tryAvoidLinkIds=", directions.TryAvoidLinkIDs) 150 | routeURL.WriteString("&stateBoundaryDisplay=" + strconv.FormatBool(directions.StateBoundaryDisplay)) 151 | routeURL.WriteString("&countryBoundaryDisplay=" + strconv.FormatBool(directions.CountryBoundaryDisplay)) 152 | routeURL.WriteString("&destinationManeuverDisplay=" + strconv.FormatBool(directions.DestinationManeuverDisplay)) 153 | routeURL.WriteString("&fullShape=" + strconv.FormatBool(directions.FullShape)) 154 | routeURL.WriteString("&cyclingRoadFactor=" + strconv.FormatFloat(directions.CyclingRoadFactor, 'f', -1, 64)) 155 | routeURL.WriteString("&roadGradeStrategy=" + directions.RoadGradeStrategy) 156 | routeURL.WriteString("&drivingStyle=" + directions.DrivingStyle) 157 | routeURL.WriteString("&highwayEfficiency=" + strconv.FormatFloat(directions.HighwayEfficiency, 'f', -1, 64)) 158 | routeURL.WriteString("&manMaps=" + strconv.FormatBool(directions.ManMaps)) 159 | routeURL.WriteString("&walkingSpeed=" + strconv.FormatFloat(directions.WalkingSpeed, 'f', -1, 64)) 160 | if directions.SessionID != "" { 161 | routeURL.WriteString("&sessionId=" + directions.SessionID) 162 | } 163 | return routeURL.String() 164 | } 165 | 166 | // Dump directions as undecoded json or xml bytes 167 | func (directions Directions) Dump(format string) (data []byte, err error) { 168 | resp, err := http.Get(directions.URL(format)) 169 | if err != nil { 170 | return 171 | } 172 | defer resp.Body.Close() 173 | data, err = ioutil.ReadAll(resp.Body) 174 | return 175 | } 176 | 177 | // Distance calculated in km or miles (unit is k [km] or m [miles]) 178 | func (directions Directions) Distance(unit string) (distance float64, err error) { 179 | // these changes are made on a copy and as such invisible to caller 180 | directions.ManMaps = false 181 | directions.NarrativeType = "none" 182 | directions.Unit = unit 183 | resp, err := http.Get(directions.URL("json")) 184 | if err != nil { 185 | return 186 | } 187 | defer resp.Body.Close() 188 | // Decode our JSON results 189 | results := DistanceResults{} 190 | err = decoder(resp).Decode(&results) 191 | if err != nil { 192 | return 193 | } 194 | if results.Info.Statuscode != 0 { 195 | err = results.Info 196 | return 197 | } 198 | distance = results.Route.Distance 199 | return 200 | } 201 | 202 | // DistanceResults json struct to retrieve only the distance 203 | type DistanceResults struct { 204 | Route struct { 205 | Distance float64 `json:"distance"` 206 | } `json:"route"` 207 | Info Info `json:"info"` 208 | } 209 | 210 | // Get the Direction Results (Route & Info) 211 | func (directions Directions) Get() (results *DirectionsResults, err error) { 212 | resp, err := http.Get(directions.URL("json")) 213 | if err != nil { 214 | return 215 | } 216 | defer resp.Body.Close() 217 | // Decode our JSON results 218 | results = &DirectionsResults{} 219 | err = decoder(resp).Decode(results) 220 | if err != nil { 221 | return 222 | } 223 | if results.Info.Statuscode != 0 { 224 | err = results.Info 225 | } 226 | return 227 | } 228 | 229 | // DirectionsResults can be decoded from the Directions route response 230 | type DirectionsResults struct { 231 | Route Route `json:"route"` 232 | Info Info `json:"info"` 233 | } 234 | 235 | // Info about copyright and statuscode 236 | type Info struct { 237 | Copyright struct { 238 | Text string `json:"text"` // "© 2014 MapQuest, Inc." 239 | ImageURL string `json:"imageUrl"` // "http://api.mqcdn.com/res/mqlogo.gif" 240 | ImageAltText string `json:"imageAltText"` // "© 2014 MapQuest, Inc." 241 | } `json:"copyright"` 242 | Statuscode int `json:"statuscode"` 243 | Messages []string `json:"messages"` 244 | } 245 | 246 | func (err Info) Error() string { 247 | if err.Statuscode != 0 { 248 | return fmt.Sprintf( 249 | "Error %d: %s", 250 | err.Statuscode, strings.Join(err.Messages, "; ")) 251 | } 252 | return "No error" 253 | } 254 | 255 | // Route provides information on how to get from one location 256 | // to one (or more) other locations. 257 | type Route struct { 258 | // Returns true if at least one leg contains a Toll Road. 259 | HasTollRoad bool `json:"hasTollRoad"` 260 | // Returns true if at least one leg contains a Limited Access/Highway. 261 | HasHighway bool `json:"hasHighway"` 262 | // Returns true if at least one leg contains a Seasonal Closure. 263 | HasSeasonalClosure bool `json:"hasSeasonalClosure"` 264 | // Returns true if at least one leg contains an Unpaved. 265 | HasUnpaved bool `json:"hasUnpaved"` 266 | // Returns true if at least one leg contains a Country Crossing. 267 | HasCountryCross bool `json:"hasCountryCross"` 268 | // Returns lat/lng bounding rectangle of all points 269 | BoundingBox struct { 270 | UpperLeft LatLng `json:"ul"` 271 | LowerRight LatLng `json:"lr"` 272 | } `json:"BoundingBox"` 273 | // Returns the calculated elapsed time in seconds for the route. 274 | Time int `json:"time"` 275 | // Returns the calculated elapsed time as formatted text in HH:MM:SS format. 276 | FormattedTime string `json:"formattedTime"` 277 | // Returns the calculated distance of the route. 278 | Distance float64 `json:"distance"` 279 | // The estimated amount of fuel used during the route 280 | FuelUsed float64 `json:"fuelUsed"` 281 | // A collection of leg objects, one for each "leg" of the route. 282 | Legs []Leg `json:"legs"` 283 | // Error report 284 | RouteError struct { 285 | Message string `json:"message"` 286 | ErrorCode int `json:"errorCode"` 287 | } `json:"routeError"` 288 | // A collection of locations in the form of an address. 289 | Locations []Location `json:"locations"` 290 | // Location sequence 291 | LocationSequence []int `json:"locationSequence"` 292 | // A unique identifier used to refer to a session 293 | SessionID string `json:"sessionId"` 294 | /* // Routing Options (not necessary as same as request) 295 | Options Options `json:"options"` */ 296 | } 297 | 298 | // Leg contains the maneuvers describing how to get from one location 299 | // to the next location. Multiple legs belong to a route. 300 | type Leg struct { 301 | // The shape point index which starts a specific route segment. 302 | Index int `json:"index"` 303 | // Returns true if at least one maneuver contains a Toll Road. 304 | HasTollRoad bool `json:"hasTollRoad"` 305 | // Returns true if at least one maneuver contains a Limited Access/Highway. 306 | HasHighway bool `json:"hasHighway"` 307 | // Returns true if at least one maneuver contains a Seasonal Closure. 308 | HasSeasonalClosure bool `json:"hasSeasonalClosure"` 309 | // Returns true if at least one maneuver contains an Unpaved. 310 | HasUnpaved bool `json:"hasUnpaved"` 311 | // Returns true if at least one maneuver contains a Country Crossing. 312 | HasCountryCross bool `json:"hasCountryCross"` 313 | // Returns the calculated elapsed time in seconds for the leg. 314 | Time int `json:"time"` 315 | // Returns the calculated elapsed time as formatted text in HH:MM:SS format. 316 | FormattedTime string `json:"formattedTime"` 317 | // Returns the calculated distance of the leg. 318 | Distance float64 `json:"distance"` 319 | // A collection of Maneuver objects 320 | Maneuvers []Maneuver `json:"maneuvers"` 321 | // Road grade avoidance strategies 322 | RoadGradeStrategy [][]int `json:"roadGradeStrategy"` 323 | 324 | // Collapsed Narrative Parameters if the user is familiar with the area 325 | 326 | // The origin index is the index of the first non-collapsed maneuver. 327 | OrigIndex int `json:"origIndex"` 328 | // The rephrased origin narrative string for the first non-collapsed maneuver. 329 | OrigNarrative string `json:"origNarrative"` 330 | // The destination index is the index of the last non-collapsed maneuver. 331 | DestIndex int `json:"destIndex"` 332 | // The rephrased destination narrative string for the destination maneuver. 333 | DestNarrative string `json:"destNarrative"` 334 | /* RoadGradeStrategy ??? `json:"roadGradeStrategy"` */ 335 | } 336 | 337 | // Maneuver describes each one step in a route narrative. 338 | // Multiple maneuvers belong to a leg. 339 | type Maneuver struct { 340 | Index int `json:"index"` 341 | // Returns the calculated elapsed time in seconds for the maneuver. 342 | Time int `json:"time"` 343 | // Returns the calculated elapsed time as formatted text in HH:MM:SS format. 344 | FormattedTime string `json:"formattedTime"` 345 | // Returns the calculated distance of the maneuver. 346 | Distance float64 `json:"distance"` 347 | // Text name, extra text, type (road shield), direction and image 348 | Signs []Sign `json:"signs"` 349 | // An URL to a static map of this maneuver. 350 | MapURL string `json:"mapUrl"` 351 | // Textual driving directions for a particular maneuver. 352 | Narrative string `json:"narrative"` 353 | /* // A collection of maneuverNote objects, one for each maneuver. 354 | ManeuverNotes []ManeuverNote `json:"maneuverNotes"` */ 355 | // none=0,north=1,northwest=2,northeast=3,south=4,southeast=5,southwest=6,west=7,east=8 356 | Direction int `json:"direction"` 357 | // Name of the direction 358 | DirectionName string `json:"directionName"` 359 | // Collection of street names this maneuver applies to 360 | Streets []string `json:"streets"` 361 | // none=0,portions toll=1,portions unpaved=2,possible seasonal road closure=4,gate=8,ferry=16,avoid id=32,country crossing=64,limited access (highways)=128 362 | Attributes int `json:"attributes"` 363 | // straight=0,slight right=1,right=2,sharp right=3,reverse=4,sharp left=5,left=6,slight left=7,right u-turn=8, 364 | // left u-turn=9,right merge=10,left merge=11,right on ramp=12,left on ramp=13,right off ramp=14,left off ramp=15,right fork=16,left fork=17,straight fork=18 365 | TurnType int `json:"direction"` 366 | // 1st shape point latLng for a particular maneuver (eg for zooming) 367 | StartPoint LatLng `json:"startPoint"` 368 | // Icon 369 | IconURL string `json:"iconUrl"` 370 | // "AUTO", "WALKING", "BICYCLE", "RAIL", "BUS" (future use) 371 | TransportMode string `json:"transportMode"` 372 | // Link Ids of roads 373 | LinkIDs []int `json:"linkIds"` 374 | } 375 | 376 | // Sign specifies information for a particular maneuver. 377 | type Sign struct { 378 | Text string `json:"text"` 379 | ExtraText string `json:"extraText"` 380 | Direction int `json:"direction"` 381 | // road shield 382 | Type int `json:"type"` 383 | // Image 384 | URL string `json:"url"` 385 | } 386 | 387 | /* TODO 388 | // Maneuver notes can exist for Timed Turn Restrictions, Timed Access Roads, HOV Roads, Seasonal Closures, and Timed Direction of Travel. 389 | type ManeuverNote struct { 390 | RuleId int `json:"ruleId"` 391 | // ... incomplete 392 | } 393 | */ 394 | --------------------------------------------------------------------------------