package models import ( "fmt" "math" "net/http" "strconv" "code.nonshy.com/nonshy/website/pkg/geoip" "code.nonshy.com/nonshy/website/pkg/log" ) // UserLocation table holds a user's location preference and coordinates. type UserLocation struct { UserID uint64 `gorm:"primaryKey"` Source string Latitude float64 `gorm:"index"` Longitude float64 `gorm:"index"` } // Source options for UserLocation. const ( LocationSourceNone = "" LocationSourceGeoIP = "geoip" LocationSourceGPS = "gps" LocationSourcePin = "pin" ) // GetUserLocation gets the UserLocation object for a user ID, or a new object. func GetUserLocation(userId uint64) *UserLocation { var ul = &UserLocation{} result := DB.First(&ul, userId) if result.Error != nil { return &UserLocation{ UserID: userId, } } return ul } // Save the UserLocation. func (ul *UserLocation) Save() error { if ul.Source == LocationSourceNone { ul.Latitude = 0 ul.Longitude = 0 } return DB.Save(ul).Error } // RefreshGeoIP will auto-update a user's location by GeoIP if that's their setting. func RefreshGeoIP(userID uint64, r *http.Request) (*UserLocation, error) { loc := GetUserLocation(userID) if loc.Source == LocationSourceGeoIP { if insights, err := geoip.GetRequestInsights(r); err == nil { loc.Latitude = truncate(insights.Latitude) loc.Longitude = truncate(insights.Longitude) return loc, loc.Save() } else { return loc, fmt.Errorf("didn't get insights: %s", err) } } return loc, nil } func truncate(f float64) float64 { s := strconv.FormatFloat(f, 'f', 2, 64) f, _ = strconv.ParseFloat(s, 64) return f } // MapDistances computes human readable distances between you and the set of users. // // The fromCity attribute is optional. If non-nil, the distance will be computed in // relation to that city's location instead of the current user. func MapDistances(currentUser *User, fromCity *WorldCities, others []*User) DistanceMap { // Get all the distances we can. var ( result = DistanceMap{} myDist = GetUserLocation(currentUser.ID) latitude, longitude = myDist.Latitude, myDist.Longitude // dist = map[uint64]*UserLocation{} userIDs = []uint64{} ) for _, user := range others { userIDs = append(userIDs, user.ID) } // Are we ordering from a different city? if fromCity != nil { latitude = fromCity.Latitude longitude = fromCity.Longitude } // Query for their UserLocation objects, if exists. var ul = []*UserLocation{} res := DB.Where("user_id IN ?", userIDs).Find(&ul) if res.Error != nil { log.Error("MapDistances: %s", res.Error) return result } // Map them out. for _, row := range ul { km, mi := HaversineDistance( latitude, longitude, row.Latitude, row.Longitude, ) result[row.UserID] = fmt.Sprintf("%.1fkm / %.1fmi", km, mi) } return result } // DistanceMap maps user IDs to distance strings. type DistanceMap map[uint64]string // Get a value from the DistanceMap for easy front-end access. func (dm DistanceMap) Get(key uint64) string { if value, ok := dm[key]; ok { return value } return "unknown distance" } // HaversineDistance returns the distance (in kilometers, miles) between // two points of latitude and longitude pairs. func HaversineDistance(lat1, lon1, lat2, lon2 float64) (float64, float64) { lat1 *= piRad lon1 *= piRad lat2 *= piRad lon2 *= piRad var r = earthRadius // Calculate. h := hsin(lat2-lat1) + math.Cos(lat1)*math.Cos(lat2)*hsin(lon2-lon1) meters := 2 * r * math.Asin(math.Sqrt(h)) kilometers := meters / 1000 miles := kilometers * 0.621371 return kilometers, miles } // adapted from: https://gist.github.com/cdipaolo/d3f8db3848278b49db68 // haversin(θ) function func hsin(theta float64) float64 { return math.Pow(math.Sin(theta/2), 2) } const ( piRad = math.Pi / 180 earthRadius = 6378100.0 // Earth radius in meters )