website/pkg/models/user_location.go
2023-08-20 10:25:13 -07:00

143 lines
3.4 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.
func MapDistances(currentUser *User, others []*User) DistanceMap {
// Get all the distances we can.
var (
result = DistanceMap{}
myDist = GetUserLocation(currentUser.ID)
// dist = map[uint64]*UserLocation{}
userIDs = []uint64{}
)
for _, user := range others {
userIDs = append(userIDs, user.ID)
}
// 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(
myDist.Latitude, myDist.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
)