├── README.md ├── geohash.go └── geohash_test.go /README.md: -------------------------------------------------------------------------------- 1 | geohash-golang 2 | ============== 3 | 4 | Golang adaptation of geohash-js (https://github.com/davetroy/geohash-js/tree/master) 5 | -------------------------------------------------------------------------------- /geohash.go: -------------------------------------------------------------------------------- 1 | // geohash.go 2 | // Geohash library for Golang 3 | // Ported from David Troy's geohash-js library (https://github.com/davetroy/geohash-js) 4 | // (c) Tomi Hiltunen 2014 5 | // Distributed under the MIT License 6 | 7 | package geohash 8 | 9 | import ( 10 | "bytes" 11 | "strings" 12 | ) 13 | 14 | var ( 15 | bits = []int{16, 8, 4, 2, 1} 16 | base32 = []byte("0123456789bcdefghjkmnpqrstuvwxyz") 17 | neighbors = [][]string{ 18 | []string{ 19 | "p0r21436x8zb9dcf5h7kjnmqesgutwvy", 20 | "bc01fg45238967deuvhjyznpkmstqrwx", 21 | }, 22 | []string{ 23 | "bc01fg45238967deuvhjyznpkmstqrwx", 24 | "p0r21436x8zb9dcf5h7kjnmqesgutwvy", 25 | }, 26 | []string{ 27 | "14365h7k9dcfesgujnmqp0r2twvyx8zb", 28 | "238967debc01fg45kmstqrwxuvhjyznp", 29 | }, 30 | []string{ 31 | "238967debc01fg45kmstqrwxuvhjyznp", 32 | "14365h7k9dcfesgujnmqp0r2twvyx8zb", 33 | }, 34 | } 35 | borders = [][]string{ 36 | []string{ 37 | "prxz", 38 | "bcfguvyz", 39 | }, 40 | []string{ 41 | "bcfguvyz", 42 | "prxz", 43 | }, 44 | []string{ 45 | "028b", 46 | "0145hjnp", 47 | }, 48 | []string{ 49 | "0145hjnp", 50 | "028b", 51 | }, 52 | } 53 | ) 54 | 55 | // Calculates adjacent geohashes. 56 | func CalculateAdjacent(s, dir string) string { 57 | s = strings.ToLower(s) 58 | lastChr := s[(len(s) - 1):] 59 | oddEven := (len(s) % 2) // 0=even; 1=odd; 60 | var dirInt int 61 | switch dir { 62 | default: 63 | dirInt = 0 64 | case "right": 65 | dirInt = 1 66 | case "bottom": 67 | dirInt = 2 68 | case "left": 69 | dirInt = 3 70 | } 71 | // base := s[0:] 72 | base := s[:(len(s) - 1)] 73 | if strings.Index(borders[dirInt][oddEven], lastChr) != -1 { 74 | base = CalculateAdjacent(base, dir) 75 | } 76 | return base + string(base32[strings.Index(neighbors[dirInt][oddEven], lastChr)]) 77 | } 78 | 79 | // Shortcut to calculate all adjacent geohashes. 80 | func CalculateAllAdjacent(s string) []string { 81 | values := []string{} 82 | directions := []string{"top", "right", "bottom", "left"} 83 | for _, direction := range directions { 84 | neighbour := CalculateAdjacent(s, direction) 85 | values = append(values, neighbour) 86 | if direction == "top" || direction == "bottom" { 87 | values = append(values, CalculateAdjacent(neighbour, "right")) 88 | values = append(values, CalculateAdjacent(neighbour, "left")) 89 | } 90 | } 91 | return values 92 | } 93 | 94 | // Struct for passing Box. 95 | type BoundingBox struct { 96 | sw LatLng 97 | ne LatLng 98 | center LatLng 99 | } 100 | 101 | // Returns coordinates for box's center. 102 | func (b *BoundingBox) Center() *LatLng { 103 | return &b.center 104 | } 105 | 106 | // Returns coordinates for box's South-West corner. 107 | func (b *BoundingBox) SouthWest() *LatLng { 108 | return &b.sw 109 | } 110 | 111 | // Returns coordinates for box's North-East corner. 112 | func (b *BoundingBox) NorthEast() *LatLng { 113 | return &b.ne 114 | } 115 | 116 | // Struct for passing LatLng values. 117 | type LatLng struct { 118 | lat float64 119 | lng float64 120 | } 121 | 122 | // Returns latitude. 123 | func (ll *LatLng) Lat() float64 { 124 | return ll.lat 125 | } 126 | 127 | // Returns longitude. 128 | func (ll *LatLng) Lng() float64 { 129 | return ll.lng 130 | } 131 | 132 | func refineInterval(interval []float64, cd, mask int) []float64 { 133 | if cd&mask > 0 { 134 | interval[0] = (interval[0] + interval[1]) / 2 135 | } else { 136 | interval[1] = (interval[0] + interval[1]) / 2 137 | } 138 | return interval 139 | } 140 | 141 | // Get LatLng coordinates from a geohash 142 | func Decode(geohash string) *BoundingBox { 143 | isEven := true 144 | lat := []float64{-90, 90} 145 | lng := []float64{-180, 180} 146 | latErr := float64(90) 147 | lngErr := float64(180) 148 | var c string 149 | var cd int 150 | for i := 0; i < len(geohash); i++ { 151 | c = geohash[i : i+1] 152 | cd = bytes.Index(base32, []byte(c)) 153 | for j := 0; j < 5; j++ { 154 | if isEven { 155 | lngErr /= 2 156 | lng = refineInterval(lng, cd, bits[j]) 157 | } else { 158 | latErr /= 2 159 | lat = refineInterval(lat, cd, bits[j]) 160 | } 161 | isEven = !isEven 162 | } 163 | } 164 | return &BoundingBox{ 165 | sw: LatLng{ 166 | lat: lat[0], 167 | lng: lng[0], 168 | }, 169 | ne: LatLng{ 170 | lat: lat[1], 171 | lng: lng[1], 172 | }, 173 | center: LatLng{ 174 | lat: (lat[0] + lat[1]) / 2, 175 | lng: (lng[0] + lng[1]) / 2, 176 | }, 177 | } 178 | } 179 | 180 | // Create a geohash with 12 positions based on LatLng coordinates 181 | func Encode(latitude, longitude float64) string { 182 | return EncodeWithPrecision(latitude, longitude, 12) 183 | } 184 | 185 | // Create a geohash with given precision (number of characters of the resulting 186 | // hash) based on LatLng coordinates 187 | func EncodeWithPrecision(latitude, longitude float64, precision int) string { 188 | isEven := true 189 | lat := []float64{-90, 90} 190 | lng := []float64{-180, 180} 191 | bit := 0 192 | ch := 0 193 | var geohash bytes.Buffer 194 | var mid float64 195 | for geohash.Len() < precision { 196 | if isEven { 197 | mid = (lng[0] + lng[1]) / 2 198 | if longitude > mid { 199 | ch |= bits[bit] 200 | lng[0] = mid 201 | } else { 202 | lng[1] = mid 203 | } 204 | } else { 205 | mid = (lat[0] + lat[1]) / 2 206 | if latitude > mid { 207 | ch |= bits[bit] 208 | lat[0] = mid 209 | } else { 210 | lat[1] = mid 211 | } 212 | } 213 | isEven = !isEven 214 | if bit < 4 { 215 | bit++ 216 | } else { 217 | geohash.WriteByte(base32[ch]) 218 | bit = 0 219 | ch = 0 220 | } 221 | } 222 | return geohash.String() 223 | } 224 | -------------------------------------------------------------------------------- /geohash_test.go: -------------------------------------------------------------------------------- 1 | package geohash 2 | 3 | import "testing" 4 | 5 | type geohashTest struct { 6 | input string 7 | output *BoundingBox 8 | } 9 | 10 | func TestDecode(t *testing.T) { 11 | var tests = []geohashTest{ 12 | geohashTest{ 13 | "d", 14 | &BoundingBox{ 15 | LatLng{0, -90}, 16 | LatLng{45, -45}, 17 | LatLng{22.5, -67.5}, 18 | }, 19 | }, 20 | geohashTest{ 21 | "dr", 22 | &BoundingBox{ 23 | LatLng{39.375, -78.75}, 24 | LatLng{45, -67.5}, 25 | LatLng{42.1875, -73.125}, 26 | }, 27 | }, 28 | geohashTest{ 29 | "dr1", 30 | &BoundingBox{ 31 | LatLng{39.375, -77.34375}, 32 | LatLng{40.78125, -75.9375}, 33 | LatLng{40.078125, -76.640625}, 34 | }, 35 | }, 36 | geohashTest{ 37 | "dr12", 38 | &BoundingBox{ 39 | LatLng{39.375, -76.9921875}, 40 | LatLng{39.55078125, -76.640625}, 41 | LatLng{39.462890625, -76.81640625}, 42 | }, 43 | }, 44 | } 45 | 46 | for _, test := range tests { 47 | box := Decode(test.input) 48 | if !equalBoundingBoxes(test.output, box) { 49 | t.Errorf("expected bounding box %v, got %v", test.output, box) 50 | } 51 | } 52 | } 53 | 54 | func equalBoundingBoxes(b1, b2 *BoundingBox) bool { 55 | return b1.ne == b2.ne && 56 | b1.sw == b2.sw && 57 | b1.center == b1.center 58 | } 59 | 60 | type encodeTest struct { 61 | latlng LatLng 62 | geohash string 63 | } 64 | 65 | func TestEncode(t *testing.T) { 66 | var tests = []encodeTest{ 67 | encodeTest{ 68 | LatLng{39.55078125, -76.640625}, 69 | "dr12zzzzzzzz", 70 | }, 71 | encodeTest{ 72 | LatLng{39.5507, -76.6406}, 73 | "dr18bpbp88fe", 74 | }, 75 | encodeTest{ 76 | LatLng{39.55, -76.64}, 77 | "dr18bpb7qw65", 78 | }, 79 | encodeTest{ 80 | LatLng{39, -76}, 81 | "dqcvyedrrwut", 82 | }, 83 | } 84 | 85 | for _, test := range tests { 86 | geohash := Encode(test.latlng.lat, test.latlng.lng) 87 | if test.geohash != geohash { 88 | t.Errorf("expectd %s, got %s", test.geohash, geohash) 89 | } 90 | } 91 | 92 | for prec := range []int{3, 4, 5, 6, 7, 8} { 93 | for _, test := range tests { 94 | geohash := EncodeWithPrecision(test.latlng.lat, test.latlng.lng, prec) 95 | if len(geohash) != prec { 96 | t.Errorf("expected len %d, got %d", prec, len(geohash)) 97 | } 98 | if test.geohash[0:prec] != geohash { 99 | t.Errorf("expectd %s, got %s", test.geohash, geohash) 100 | } 101 | } 102 | } 103 | } 104 | --------------------------------------------------------------------------------