├── .gitignore ├── README.md ├── bin ├── .keep ├── server_amd64 └── server_darwin ├── build_linux.sh ├── data ├── .keep └── master.csv.gz ├── src ├── dummy_test_data.csv.gz ├── server.go └── server_test.go └── start.sh /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | dump.rdb 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Loc2Country 2 | 3 | Location coordinates (lat/lon) to ISO alpha-3 country code. Responds in microseconds. 4 | 5 | ## Manual 6 | 7 | Input format: latitude, longitude 8 | 9 | Output format: 3-letter-ISO-country-code, time-taken-to-respond-in-nanos 10 | 11 | ## HowTo 12 | 13 | 1. Run start.sh 14 | 2. This will start a TCP server (localhost:3333) by default. 15 | 3. Connect to the server by using telnet. (eg: "telnet localhost 3333") 16 | 4. Input lat and lon seperated by comma, returns 3 letter country code and time taken to respond in nanoseconds. 17 | 18 | ## Compiling 19 | 20 | To compile, run: 21 | 22 | ``` bash 23 | go build src/server.go 24 | ``` 25 | 26 | To compile for a Linux machine from Mac, run (with correct architecture): 27 | 28 | ``` bash 29 | env GOOS=linux GOARCH=amd64 go build src/server.go 30 | ``` 31 | 32 | ## Testing 33 | 34 | To test, run: 35 | 36 | ``` 37 | go test 38 | ``` 39 | 40 | 41 | ## Example 42 | 43 | Starting the server: 44 | 45 | ``` bash 46 | $ sh start.sh 47 | 2016/08/18 23:30:07 Creating server with address localhost:3333 48 | 2016/08/18 23:30:07 Loading data.. 49 | 2016/08/18 23:30:13 Loading complete. 50 | 2016/08/18 23:30:13 Total Entries: 5235316 51 | 2016/08/18 23:30:13 Boot time: 5 seconds 52 | ``` 53 | 54 | ``` bash 55 | $ telnet 127.0.0.1 3333 56 | Trying 127.0.0.1... 57 | Connected to localhost. 58 | Escape character is '^]'. 59 | 12,77 60 | IND,17176 61 | ``` 62 | 63 | ## Data 64 | 65 | The world boundaries were generated using QGIS, converted to a set of ~350 million geohashes at precision level 6 and then reduced (compressed) to a set of ~5 million geohashes using [georaptor](https://github.com/ashwin711/georaptor). 66 | 67 | 68 | ## Contributors 69 | 70 | Sooraj B - [@soorajb](http://github.com/soorajb) 71 | 72 | Ashwin Nair - [@ashwin711](http://github.com/ashwin711) 73 | 74 | Harikrishnan Shaji - [@har777](http://github.com/har777) 75 | 76 | ## License 77 | 78 | MIT License 79 | -------------------------------------------------------------------------------- /bin/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soorajb/loc2country/0e5b32e132fba889bc9b89da8b1211aa62b61840/bin/.keep -------------------------------------------------------------------------------- /bin/server_amd64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soorajb/loc2country/0e5b32e132fba889bc9b89da8b1211aa62b61840/bin/server_amd64 -------------------------------------------------------------------------------- /bin/server_darwin: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soorajb/loc2country/0e5b32e132fba889bc9b89da8b1211aa62b61840/bin/server_darwin -------------------------------------------------------------------------------- /build_linux.sh: -------------------------------------------------------------------------------- 1 | env GOOS=linux GOARCH=amd64 go build src/server.go 2 | -------------------------------------------------------------------------------- /data/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soorajb/loc2country/0e5b32e132fba889bc9b89da8b1211aa62b61840/data/.keep -------------------------------------------------------------------------------- /data/master.csv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soorajb/loc2country/0e5b32e132fba889bc9b89da8b1211aa62b61840/data/master.csv.gz -------------------------------------------------------------------------------- /src/dummy_test_data.csv.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/soorajb/loc2country/0e5b32e132fba889bc9b89da8b1211aa62b61840/src/dummy_test_data.csv.gz -------------------------------------------------------------------------------- /src/server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "compress/gzip" 6 | "flag" 7 | "log" 8 | "os" 9 | "strconv" 10 | "strings" 11 | "time" 12 | 13 | "github.com/firstrow/tcp_server" 14 | "github.com/mmcloughlin/geohash" 15 | ) 16 | 17 | var serverHost = flag.String("host", "localhost", "Hostname to bind to. eg: 192.168.1.10, default:localhost") 18 | var serverPort = flag.String("port", "3333", "Port eg: 8080, default: 3333") 19 | var dataPath = flag.String("dataPath", "./data/master.csv.gz", "Path to data file. eg: ./data/file.gz, default: ./data/master.csv.gz") 20 | 21 | func main() { 22 | flag.Parse() 23 | connString := *serverHost + ":" + *serverPort 24 | dataFilePath := *dataPath 25 | 26 | server := tcp_server.New(connString) 27 | geohashToCountryMapping := getGeohashToCountryMapping(dataFilePath) 28 | 29 | server.OnNewMessage(func(c *tcp_server.Client, message string) { 30 | startNano := time.Now().UnixNano() 31 | reply := messageHandler(message, geohashToCountryMapping) 32 | c.Send(reply + "\n") 33 | log.Println("Response time: " + strconv.FormatInt((time.Now().UnixNano()-startNano), 10)) 34 | }) 35 | server.Listen() 36 | } 37 | 38 | func messageHandler(message string, geohashToCountryMapping map[string]string) string { 39 | message = strings.TrimSpace(message) 40 | response := "" 41 | startNano := time.Now().UnixNano() 42 | if message != "" { 43 | lat, lon := parseLatLonFromMessage(message) 44 | geohash6 := generateGeohash(lat, lon) 45 | country := getCountryFromGeohashToCountryMapping(geohash6, geohashToCountryMapping) 46 | timeTaken := time.Now().UnixNano() - startNano 47 | response = country + ", " + strconv.FormatInt(timeTaken, 10) 48 | } 49 | 50 | log.Println(message + " => " + response) 51 | return response 52 | } 53 | 54 | func getCountryFromGeohashToCountryMapping(geohash6 string, geohashToCountryMapping map[string]string) string { 55 | country := "" 56 | for geohashLength := 6; geohashLength > 1; geohashLength-- { 57 | country = geohashToCountryMapping[geohash6[0:geohashLength]] 58 | if country != "" { 59 | break 60 | } 61 | } 62 | return country 63 | } 64 | 65 | func parseLatLonFromMessage(message string) (float64, float64) { 66 | coords := strings.Split(message, ",") 67 | lat, lon := 0.0, 0.0 68 | if len(coords) == 2 { 69 | lat, _ = strconv.ParseFloat(strings.TrimSpace(coords[0]), 64) 70 | lon, _ = strconv.ParseFloat(strings.TrimSpace(coords[1]), 64) 71 | } 72 | return lat, lon 73 | } 74 | 75 | func generateGeohash(lat float64, lon float64) string { 76 | return geohash.EncodeWithPrecision(lat, lon, 6) 77 | } 78 | 79 | func getGeohashToCountryMapping(filePath string) map[string]string { 80 | geohashToCountryMapping := make(map[string]string) 81 | startNano := time.Now().UnixNano() 82 | log.Println("Loading data.") 83 | file, err := os.Open(filePath) 84 | defer file.Close() 85 | checkError(err) 86 | 87 | gzipReader, err := gzip.NewReader(file) 88 | defer gzipReader.Close() 89 | checkError(err) 90 | 91 | scanner := bufio.NewScanner(gzipReader) 92 | for scanner.Scan() { 93 | line := strings.Split(scanner.Text(), ",") 94 | geohashToCountryMapping[line[0]] = line[1] 95 | } 96 | 97 | checkError(scanner.Err()) 98 | 99 | log.Println("Loading complete.") 100 | log.Println("Total Entries: " + strconv.Itoa(len(geohashToCountryMapping))) 101 | timeTaken := (time.Now().UnixNano() - startNano) / 1000000000 102 | log.Println("Boot time: " + strconv.FormatInt(timeTaken, 10) + " seconds") 103 | return geohashToCountryMapping 104 | } 105 | 106 | func checkError(e error) { 107 | if e != nil { 108 | log.Println(e) 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/server_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGenerateGeohash(t *testing.T) { 10 | lat := 12.941084 11 | lon := 77.6099103 12 | expectedGeohash := "tdr1w5" 13 | actualGeohash := generateGeohash(lat, lon) 14 | assert.Equal(t, expectedGeohash, actualGeohash, "Generated geohash doesnt match with expected geohash.") 15 | } 16 | 17 | func TestParseLatLonFromMessage1(t *testing.T) { 18 | message := "12.941084,77.6099103" 19 | expectedLat, expectedLon := 12.941084, 77.6099103 20 | actualLat, actualLon := parseLatLonFromMessage(message) 21 | assert.Equal(t, expectedLat, actualLat, "Generated latitude doesnt match with expected latitude.") 22 | assert.Equal(t, expectedLon, actualLon, "Generated longitude doesnt match with expected longitude.") 23 | } 24 | 25 | func TestParseLatLonFromMessage2(t *testing.T) { 26 | message := "40.744630, -73.981481" 27 | expectedLat, expectedLon := 40.744630, -73.981481 28 | actualLat, actualLon := parseLatLonFromMessage(message) 29 | assert.Equal(t, expectedLat, actualLat, "Generated latitude doesnt match with expected latitude.") 30 | assert.Equal(t, expectedLon, actualLon, "Generated longitude doesnt match with expected longitude.") 31 | } 32 | 33 | func TestGetCountryFromGeohashToCountryMapping(t *testing.T) { 34 | geohash6FromCountryAUS, expectedCountryForAUSGeohash := "qsxtqw", "AUS" 35 | geohash6FromCountryJPN, expectedCountryForJPNGeohash := "xn7735", "JPN" 36 | geohash6FromOcean, expectedCountryForOceanGeohash := "mxty6f", "" 37 | 38 | geohashToCountryMapping := make(map[string]string) 39 | geohashToCountryMapping["qsxtqw"] = "AUS" 40 | geohashToCountryMapping["xn7"] = "JPN" 41 | 42 | actualCountryForAUSGeohash := getCountryFromGeohashToCountryMapping(geohash6FromCountryAUS, geohashToCountryMapping) 43 | actualCountryForJPNGeohash := getCountryFromGeohashToCountryMapping(geohash6FromCountryJPN, geohashToCountryMapping) 44 | actualCountryForOceanGeohash := getCountryFromGeohashToCountryMapping(geohash6FromOcean, geohashToCountryMapping) 45 | 46 | assert.Equal(t, expectedCountryForAUSGeohash, actualCountryForAUSGeohash, "Generated country doesnt match with expected country.") 47 | assert.Equal(t, expectedCountryForJPNGeohash, actualCountryForJPNGeohash, "Generated country doesnt match with expected country.") 48 | assert.Equal(t, expectedCountryForOceanGeohash, actualCountryForOceanGeohash, "Generated country doesnt match with expected country.") 49 | } 50 | 51 | func TestGetGeohashToCountryMapping(t *testing.T) { 52 | expectedGeohashToCountryMapping := make(map[string]string) 53 | expectedGeohashToCountryMapping["d6nq9t"] = "ABW" 54 | expectedGeohashToCountryMapping["wh3x0u"] = "IND" 55 | 56 | actualGeohashToCountryMapping := getGeohashToCountryMapping("dummy_test_data.csv.gz") 57 | 58 | assert.Equal(t, expectedGeohashToCountryMapping, actualGeohashToCountryMapping, "Generated geohashToCountryMapping doesnt match with expected geohashToCountryMapping.") 59 | } 60 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | nohup ./bin/server -p 3333 > /dev/null & 2 | --------------------------------------------------------------------------------