b8be14ea8d
* 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.
153 lines
3.8 KiB
Go
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
|
|
)
|