diff options
-rw-r--r-- | README.markdown | 17 | ||||
-rw-r--r-- | database.go | 19 | ||||
-rw-r--r-- | main.go | 55 |
3 files changed, 84 insertions, 7 deletions
diff --git a/README.markdown b/README.markdown index 887b833..6ac6da7 100644 --- a/README.markdown +++ b/README.markdown @@ -15,6 +15,23 @@ To get zipcode info using `curl` and [`jq`](https://stedolan.github.io/jq/): curl --silent http://localhost:8080/zip/18101 | jq '.' ``` +# Response + +Your response will look something like this: + +```json +{ + "latitude": 40.602847, + "longitude": -75.47022, + "city": "Allentown", + "state": "PA" +} +``` + +If the zipcode you request isn't in the database, you'll get a `404` status with an empty body. + +Add a `?distance=` parameter to your request to see how far away two zipcodes are. The response format will be the same as above with an additional `distance` key that contains info about the second zipcode as well as `miles` and `kilometers` keys. If the second zipcode can't be found, the `distance` key won't be included. + # Data This project uses public-domain zipcode data from the [Zip Code Database Project](http://zips.sourceforge.net). diff --git a/database.go b/database.go index 964915c..f0016bc 100644 --- a/database.go +++ b/database.go @@ -2,6 +2,7 @@ package main import ( "encoding/csv" + "math" "os" "strconv" "strings" @@ -14,6 +15,24 @@ type ZipcodeDetails struct { State string `json:"state"` } +const EarthRadiusMiles float64 = 3960 +const EarthRadiusKm float64 = 6373 +const DegToRad float64 = math.Pi / 180 + +func (zip1 *ZipcodeDetails) DistanceTo(zip2 *ZipcodeDetails) (float64, float64) { + // the arc distance ψ = arccos(sin φ_1 sin φ_2 cos(θ_1-θ_2) + cos φ_1 cos φ_2) + // where + // φ_k = 90° - latitude_k + // θ_k = longitude_k + // (both in radians) + theta1 := zip1.Longitude * DegToRad + theta2 := zip2.Longitude * DegToRad + phi1 := (90 - zip1.Latitude) * DegToRad + phi2 := (90 - zip2.Latitude) * DegToRad + arc := math.Acos(math.Sin(phi1)*math.Sin(phi2)*math.Cos(theta1-theta2) + math.Cos(phi1)*math.Cos(phi2)) + return arc * EarthRadiusMiles, arc * EarthRadiusKm +} + type ZipcodeDatabase struct { Zipcodes map[string]*ZipcodeDetails } @@ -8,6 +8,17 @@ import ( "strings" ) +type DistanceZipcode struct { + ZipcodeDetails + Miles float64 `json:"miles"` + Kilometers float64 `json:"kilometers"` +} + +type DistanceResponse struct { + ZipcodeDetails + Distance DistanceZipcode `json:"distance"` +} + func main() { db := getInitializedDatabase() http.HandleFunc("/zip/", func(w http.ResponseWriter, r *http.Request) { @@ -37,25 +48,55 @@ func getListeningAddress() string { } func handleZipcodeRequest(response http.ResponseWriter, request *http.Request, db *ZipcodeDatabase) { - zip := zipcodeForRequest(request) - if details := db.Find(zip); details == nil { + zip := zipcodeForPath(request, db) + if zip == nil { http.Error(response, "", 404) } else { setResponseHeaders(response) - sendZipcodeDetails(details, response) + if zip2 := zipcodeForQuery(request, db); zip2 != nil { + sendDistanceResponse(zip, zip2, response) + } else { + sendJson(response, zip) + } } } +func sendDistanceResponse(zip1, zip2 *ZipcodeDetails, writer http.ResponseWriter) { + mi, km := zip1.DistanceTo(zip2) + response := &DistanceResponse{} + response.ZipcodeDetails = *zip1 + response.Distance.ZipcodeDetails = *zip2 + response.Distance.Miles = mi + response.Distance.Kilometers = km + sendJson(writer, response) +} + +func sendJson(writer http.ResponseWriter, data interface{}) { + bytes, _ := json.MarshalIndent(data, "", " ") + fmt.Fprintf(writer, string(bytes)) +} + func setResponseHeaders(response http.ResponseWriter) { response.Header().Set("Access-Control-Allow-Origin", "*") response.Header().Set("Content-Type", "application/json") } -func zipcodeForRequest(request *http.Request) string { +func extractZipFromPath(request *http.Request) string { return request.URL.Path[len("/zip/"):] } -func sendZipcodeDetails(details *ZipcodeDetails, response http.ResponseWriter) { - data, _ := json.MarshalIndent(details, "", " ") - fmt.Fprintf(response, string(data)) +func zipcodeForPath(request *http.Request, db *ZipcodeDatabase) *ZipcodeDetails { + return db.Find(extractZipFromPath(request)) +} + +func extractZipFromQuery(request *http.Request) string { + if param := request.URL.Query()["distance"]; param != nil { + return param[0] + } else { + return "" + } +} + +func zipcodeForQuery(request *http.Request, db *ZipcodeDatabase) *ZipcodeDetails { + return db.Find(extractZipFromQuery(request)) } |