Files
wanderer/db/routes/waypoint_cluster.go
Flomp d2ac49470a Federation Refactoring & Architecture Improvements (#930)
* initial commit

* add lists

* update permissions

* fix waypoint create

* needs_full_sync for activitypub trails

* fixes build issues

* fix list get

* further CSRF protection

* update API docs

* adds rate limiter

* fix tiptap mentions

* a bit more cleanup of main.go

* Fix migration order

* fixes reviewed notes

* require context for activitpub server calls

* improve hashing for identifier

* fix copy paste error

* fix sync trail/list issues

* fix summit log/comment duplicates

* adaptions after trail merge

* fix dockerignore

---------

Co-authored-by: Christian Beutel <>
Co-authored-by: slothful-vassal <89943360+slothful-vassal@users.noreply.github.com>
2026-05-09 17:30:34 +02:00

198 lines
5.0 KiB
Go

package routes
import (
"net/http"
"github.com/pocketbase/pocketbase/apis"
"github.com/pocketbase/pocketbase/core"
"pocketbase/util"
)
const defaultWaypointMergeRadius = 50
type waypointMergeSettings struct {
Enabled bool
Radius float64
}
type waypointClusterRequest struct {
Category string `json:"category"`
Photos []waypointClusterPhoto `json:"photos"`
Waypoints []waypointClusterWaypoint `json:"waypoints"`
}
type waypointClusterPhoto struct {
ID string `json:"id"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
type waypointClusterWaypoint struct {
ID string `json:"id"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
type waypointPhotoCluster struct {
Waypoint string `json:"waypoint,omitempty"`
Photos []string `json:"photos"`
SumLat float64 `json:"-"`
SumLon float64 `json:"-"`
Count int `json:"-"`
Lat float64 `json:"lat"`
Lon float64 `json:"lon"`
}
type categorySettings struct {
WaypointMergeEnabled *bool `json:"wp_merge_enabled"`
WaypointMergeRadius *float64 `json:"wp_merge_radius"`
}
func WaypointCluster(e *core.RequestEvent) error {
if e.Auth == nil {
return apis.NewUnauthorizedError("authentication required", nil)
}
var data waypointClusterRequest
if err := e.BindBody(&data); err != nil {
return apis.NewBadRequestError("Failed to read request data", err)
}
if data.Category != "" && len(data.Category) != 15 {
return apis.NewBadRequestError("Invalid category", nil)
}
for _, photo := range data.Photos {
if photo.ID == "" {
return apis.NewBadRequestError("Invalid photo id", nil)
}
if photo.Lat < -90 || photo.Lat > 90 {
return apis.NewBadRequestError("Invalid photo latitude", nil)
}
if photo.Lon < -180 || photo.Lon > 180 {
return apis.NewBadRequestError("Invalid photo longitude", nil)
}
}
for _, waypoint := range data.Waypoints {
if waypoint.ID == "" {
return apis.NewBadRequestError("Invalid waypoint id", nil)
}
if waypoint.Lat < -90 || waypoint.Lat > 90 {
return apis.NewBadRequestError("Invalid waypoint latitude", nil)
}
if waypoint.Lon < -180 || waypoint.Lon > 180 {
return apis.NewBadRequestError("Invalid waypoint longitude", nil)
}
}
mergeSettings, err := getWaypointMergeSettings(e.App, data.Category)
if err != nil {
return err
}
return e.JSON(http.StatusOK, map[string]any{
"mergeEnabled": mergeSettings.Enabled,
"mergeRadius": mergeSettings.Radius,
"clusters": clusterWaypointPhotos(data.Photos, data.Waypoints, mergeSettings),
})
}
func getWaypointMergeSettings(app core.App, categoryId string) (waypointMergeSettings, error) {
defaultSettings := waypointMergeSettings{
Enabled: true,
Radius: defaultWaypointMergeRadius,
}
if categoryId == "" {
return defaultSettings, nil
}
category, err := app.FindRecordById("categories", categoryId)
if err != nil {
return waypointMergeSettings{}, err
}
var settings categorySettings
if err := category.UnmarshalJSONField("settings", &settings); err != nil {
return defaultSettings, nil
}
if settings.WaypointMergeEnabled != nil {
defaultSettings.Enabled = *settings.WaypointMergeEnabled
}
if settings.WaypointMergeRadius != nil && *settings.WaypointMergeRadius >= 0 {
defaultSettings.Radius = *settings.WaypointMergeRadius
}
return defaultSettings, nil
}
func clusterWaypointPhotos(photos []waypointClusterPhoto, waypoints []waypointClusterWaypoint, mergeSettings waypointMergeSettings) []waypointPhotoCluster {
clusters := []waypointPhotoCluster{}
if mergeSettings.Enabled {
for _, waypoint := range waypoints {
clusters = append(clusters, newWaypointCluster(waypoint))
}
}
for _, photo := range photos {
if !mergeSettings.Enabled {
clusters = append(clusters, newWaypointPhotoCluster(photo))
continue
}
matchingClusterIndex := -1
for i, cluster := range clusters {
distanceToCenter := util.HaversineDistance(cluster.Lat, cluster.Lon, photo.Lat, photo.Lon)
if distanceToCenter <= mergeSettings.Radius {
matchingClusterIndex = i
break
}
}
if matchingClusterIndex >= 0 {
addPhotoToWaypointCluster(&clusters[matchingClusterIndex], photo)
} else {
clusters = append(clusters, newWaypointPhotoCluster(photo))
}
}
return clusters
}
func newWaypointPhotoCluster(photo waypointClusterPhoto) waypointPhotoCluster {
return waypointPhotoCluster{
Photos: []string{photo.ID},
SumLat: photo.Lat,
SumLon: photo.Lon,
Count: 1,
Lat: photo.Lat,
Lon: photo.Lon,
}
}
func newWaypointCluster(waypoint waypointClusterWaypoint) waypointPhotoCluster {
return waypointPhotoCluster{
Waypoint: waypoint.ID,
Photos: []string{},
SumLat: waypoint.Lat,
SumLon: waypoint.Lon,
Count: 1,
Lat: waypoint.Lat,
Lon: waypoint.Lon,
}
}
func addPhotoToWaypointCluster(cluster *waypointPhotoCluster, photo waypointClusterPhoto) {
cluster.Photos = append(cluster.Photos, photo.ID)
cluster.SumLat += photo.Lat
cluster.SumLon += photo.Lon
cluster.Count++
cluster.Lat = cluster.SumLat / float64(cluster.Count)
cluster.Lon = cluster.SumLon / float64(cluster.Count)
}