summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBen Burwell <ben@benburwell.com>2015-11-19 11:02:12 -0500
committerBen Burwell <ben@benburwell.com>2015-11-19 11:02:12 -0500
commit81f58028b6e7473a78d8de0070bd6511287fbe0a (patch)
tree7b41af196f9a549d2067826aebcc6c1f26998776
parentfe22af7afdd2cffbd8395c7151f6e957db36f065 (diff)
parent5be5fe170fd317a0ed5b85b513ccf3f52e433521 (diff)
Merge pull request #1 from benburwell/distance-calc
Add distance calculation functionality
-rw-r--r--README.markdown17
-rw-r--r--database.go19
-rw-r--r--main.go55
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
}
diff --git a/main.go b/main.go
index 18342ed..0df0cce 100644
--- a/main.go
+++ b/main.go
@@ -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))
}