github.com/hdt3213/godis@v1.2.9/database/geo.go (about) 1 package database 2 3 import ( 4 "fmt" 5 "github.com/hdt3213/godis/datastruct/sortedset" 6 "github.com/hdt3213/godis/interface/redis" 7 "github.com/hdt3213/godis/lib/geohash" 8 "github.com/hdt3213/godis/lib/utils" 9 "github.com/hdt3213/godis/redis/protocol" 10 "strconv" 11 "strings" 12 ) 13 14 // execGeoAdd add a location into SortedSet 15 func execGeoAdd(db *DB, args [][]byte) redis.Reply { 16 if len(args) < 4 || len(args)%3 != 1 { 17 return protocol.MakeErrReply("ERR wrong number of arguments for 'geoadd' command") 18 } 19 key := string(args[0]) 20 size := (len(args) - 1) / 3 21 elements := make([]*sortedset.Element, size) 22 for i := 0; i < size; i++ { 23 lngStr := string(args[3*i+1]) 24 latStr := string(args[3*i+2]) 25 lng, err := strconv.ParseFloat(lngStr, 64) 26 if err != nil { 27 return protocol.MakeErrReply("ERR value is not a valid float") 28 } 29 lat, err := strconv.ParseFloat(latStr, 64) 30 if err != nil { 31 return protocol.MakeErrReply("ERR value is not a valid float") 32 } 33 if lat < -90 || lat > 90 || lng < -180 || lng > 180 { 34 return protocol.MakeErrReply(fmt.Sprintf("ERR invalid longitude,latitude pair %s,%s", latStr, lngStr)) 35 } 36 code := float64(geohash.Encode(lat, lng)) 37 elements[i] = &sortedset.Element{ 38 Member: string(args[3*i+3]), 39 Score: code, 40 } 41 } 42 43 // get or init entity 44 sortedSet, _, errReply := db.getOrInitSortedSet(key) 45 if errReply != nil { 46 return errReply 47 } 48 49 i := 0 50 for _, e := range elements { 51 if sortedSet.Add(e.Member, e.Score) { 52 i++ 53 } 54 } 55 db.addAof(utils.ToCmdLine3("geoadd", args...)) 56 return protocol.MakeIntReply(int64(i)) 57 } 58 59 func undoGeoAdd(db *DB, args [][]byte) []CmdLine { 60 key := string(args[0]) 61 size := (len(args) - 1) / 3 62 fields := make([]string, size) 63 for i := 0; i < size; i++ { 64 fields[i] = string(args[3*i+3]) 65 } 66 return rollbackZSetFields(db, key, fields...) 67 } 68 69 // execGeoPos returns location of a member 70 func execGeoPos(db *DB, args [][]byte) redis.Reply { 71 // parse args 72 if len(args) < 1 { 73 return protocol.MakeErrReply("ERR wrong number of arguments for 'geopos' command") 74 } 75 key := string(args[0]) 76 sortedSet, errReply := db.getAsSortedSet(key) 77 if errReply != nil { 78 return errReply 79 } 80 if sortedSet == nil { 81 return &protocol.NullBulkReply{} 82 } 83 84 positions := make([]redis.Reply, len(args)-1) 85 for i := 0; i < len(args)-1; i++ { 86 member := string(args[i+1]) 87 elem, exists := sortedSet.Get(member) 88 if !exists { 89 positions[i] = &protocol.EmptyMultiBulkReply{} 90 continue 91 } 92 lat, lng := geohash.Decode(uint64(elem.Score)) 93 lngStr := strconv.FormatFloat(lng, 'f', -1, 64) 94 latStr := strconv.FormatFloat(lat, 'f', -1, 64) 95 positions[i] = protocol.MakeMultiBulkReply([][]byte{ 96 []byte(lngStr), []byte(latStr), 97 }) 98 } 99 return protocol.MakeMultiRawReply(positions) 100 } 101 102 // execGeoDist returns the distance between two locations 103 func execGeoDist(db *DB, args [][]byte) redis.Reply { 104 // parse args 105 if len(args) != 3 && len(args) != 4 { 106 return protocol.MakeErrReply("ERR wrong number of arguments for 'geodist' command") 107 } 108 key := string(args[0]) 109 sortedSet, errReply := db.getAsSortedSet(key) 110 if errReply != nil { 111 return errReply 112 } 113 if sortedSet == nil { 114 return &protocol.NullBulkReply{} 115 } 116 117 positions := make([][]float64, 2) 118 for i := 1; i < 3; i++ { 119 member := string(args[i]) 120 elem, exists := sortedSet.Get(member) 121 if !exists { 122 return &protocol.NullBulkReply{} 123 } 124 lat, lng := geohash.Decode(uint64(elem.Score)) 125 positions[i-1] = []float64{lat, lng} 126 } 127 unit := "m" 128 if len(args) == 4 { 129 unit = strings.ToLower(string(args[3])) 130 } 131 dis := geohash.Distance(positions[0][0], positions[0][1], positions[1][0], positions[1][1]) 132 switch unit { 133 case "m": 134 disStr := strconv.FormatFloat(dis, 'f', -1, 64) 135 return protocol.MakeBulkReply([]byte(disStr)) 136 case "km": 137 disStr := strconv.FormatFloat(dis/1000, 'f', -1, 64) 138 return protocol.MakeBulkReply([]byte(disStr)) 139 } 140 return protocol.MakeErrReply("ERR unsupported unit provided. please use m, km") 141 } 142 143 // execGeoHash return geo-hash-code of given position 144 func execGeoHash(db *DB, args [][]byte) redis.Reply { 145 // parse args 146 if len(args) < 1 { 147 return protocol.MakeErrReply("ERR wrong number of arguments for 'geohash' command") 148 } 149 150 key := string(args[0]) 151 sortedSet, errReply := db.getAsSortedSet(key) 152 if errReply != nil { 153 return errReply 154 } 155 if sortedSet == nil { 156 return &protocol.NullBulkReply{} 157 } 158 159 strs := make([][]byte, len(args)-1) 160 for i := 0; i < len(args)-1; i++ { 161 member := string(args[i+1]) 162 elem, exists := sortedSet.Get(member) 163 if !exists { 164 strs[i] = (&protocol.EmptyMultiBulkReply{}).ToBytes() 165 continue 166 } 167 str := geohash.ToString(geohash.FromInt(uint64(elem.Score))) 168 strs[i] = []byte(str) 169 } 170 return protocol.MakeMultiBulkReply(strs) 171 } 172 173 // execGeoRadius returns members within max distance of given point 174 func execGeoRadius(db *DB, args [][]byte) redis.Reply { 175 // parse args 176 if len(args) < 5 { 177 return protocol.MakeErrReply("ERR wrong number of arguments for 'georadius' command") 178 } 179 180 key := string(args[0]) 181 sortedSet, errReply := db.getAsSortedSet(key) 182 if errReply != nil { 183 return errReply 184 } 185 if sortedSet == nil { 186 return &protocol.NullBulkReply{} 187 } 188 189 lng, err := strconv.ParseFloat(string(args[1]), 64) 190 if err != nil { 191 return protocol.MakeErrReply("ERR value is not a valid float") 192 } 193 lat, err := strconv.ParseFloat(string(args[2]), 64) 194 if err != nil { 195 return protocol.MakeErrReply("ERR value is not a valid float") 196 } 197 radius, err := strconv.ParseFloat(string(args[3]), 64) 198 if err != nil { 199 return protocol.MakeErrReply("ERR value is not a valid float") 200 } 201 unit := strings.ToLower(string(args[4])) 202 if unit == "m" { 203 } else if unit == "km" { 204 radius *= 1000 205 } else { 206 return protocol.MakeErrReply("ERR unsupported unit provided. please use m, km") 207 } 208 return geoRadius0(sortedSet, lat, lng, radius) 209 } 210 211 // execGeoRadiusByMember returns members within max distance of given member's location 212 func execGeoRadiusByMember(db *DB, args [][]byte) redis.Reply { 213 // parse args 214 if len(args) < 3 { 215 return protocol.MakeErrReply("ERR wrong number of arguments for 'georadiusbymember' command") 216 } 217 218 key := string(args[0]) 219 sortedSet, errReply := db.getAsSortedSet(key) 220 if errReply != nil { 221 return errReply 222 } 223 if sortedSet == nil { 224 return &protocol.NullBulkReply{} 225 } 226 227 member := string(args[1]) 228 elem, ok := sortedSet.Get(member) 229 if !ok { 230 return &protocol.NullBulkReply{} 231 } 232 lat, lng := geohash.Decode(uint64(elem.Score)) 233 234 radius, err := strconv.ParseFloat(string(args[2]), 64) 235 if err != nil { 236 return protocol.MakeErrReply("ERR value is not a valid float") 237 } 238 if len(args) > 3 { 239 unit := strings.ToLower(string(args[3])) 240 if unit == "m" { 241 } else if unit == "km" { 242 radius *= 1000 243 } else { 244 return protocol.MakeErrReply("ERR unsupported unit provided. please use m, km") 245 } 246 } 247 return geoRadius0(sortedSet, lat, lng, radius) 248 } 249 250 func geoRadius0(sortedSet *sortedset.SortedSet, lat float64, lng float64, radius float64) redis.Reply { 251 areas := geohash.GetNeighbours(lat, lng, radius) 252 members := make([][]byte, 0) 253 for _, area := range areas { 254 lower := &sortedset.ScoreBorder{Value: float64(area[0])} 255 upper := &sortedset.ScoreBorder{Value: float64(area[1])} 256 elements := sortedSet.RangeByScore(lower, upper, 0, -1, true) 257 for _, elem := range elements { 258 members = append(members, []byte(elem.Member)) 259 } 260 } 261 return protocol.MakeMultiBulkReply(members) 262 } 263 264 func init() { 265 registerCommand("GeoAdd", execGeoAdd, writeFirstKey, undoGeoAdd, -5, flagWrite). 266 attachCommandExtra([]string{redisFlagWrite, redisFlagDenyOOM}, 1, 1, 1) 267 registerCommand("GeoPos", execGeoPos, readFirstKey, nil, -2, flagReadOnly). 268 attachCommandExtra([]string{redisFlagReadonly}, 1, 1, 1) 269 registerCommand("GeoDist", execGeoDist, readFirstKey, nil, -4, flagReadOnly). 270 attachCommandExtra([]string{redisFlagReadonly}, 1, 1, 1) 271 registerCommand("GeoHash", execGeoHash, readFirstKey, nil, -2, flagReadOnly). 272 attachCommandExtra([]string{redisFlagReadonly}, 1, 1, 1) 273 registerCommand("GeoRadius", execGeoRadius, readFirstKey, nil, -6, flagReadOnly). 274 attachCommandExtra([]string{redisFlagWrite, redisFlagMovableKeys}, 1, 1, 1) 275 registerCommand("GeoRadiusByMember", execGeoRadiusByMember, readFirstKey, nil, -5, flagReadOnly). 276 attachCommandExtra([]string{redisFlagWrite, redisFlagMovableKeys}, 1, 1, 1) 277 }