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  }