website/pkg/models/user_location.go
Noah Petherbridge b8be14ea8d Search By Location
* Add a world cities database with type-ahead search on the Member Directory.
* Users can search for a known city to order users by distance from that city
  rather than from their own configured location on their settings page.
* Users must opt-in their own location before this feature may be used, in order
  to increase adoption of the location feature and to enforce fairness.
* The `nonshy setup locations` command can import the world cities database.
2024-08-03 14:54:22 -07:00

153 lines
3.8 KiB
Go

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
)