Trail Edit / Waypoints from photos: prevent creating different waypoints for near-by locations (#457)
* trail edit / waypoints from photos: prevent creating different waypoints for near-by locations * remove not necessary imports * remove changes from gitignore * patch gitignore * waypoint merge radius as category property * change datamodel for merge radius (use more generic settings field for future implementations) * fix npm run check issues * several improvements/fixes * fix * move geo cluster code to db * fix compile issue * fix docker issue * merge with existing waypoints --------- Co-authored-by: Christian Beutel <> Co-authored-by: Flomp <Flomp@users.noreply.github.com>
This commit is contained in:
@@ -6,4 +6,6 @@
|
||||
!main.go
|
||||
!migrations
|
||||
!templates
|
||||
!waypointcluster
|
||||
!waypointcluster/**
|
||||
!util
|
||||
|
||||
@@ -31,6 +31,7 @@ import (
|
||||
|
||||
_ "pocketbase/migrations"
|
||||
"pocketbase/util"
|
||||
"pocketbase/waypointcluster"
|
||||
|
||||
pub "github.com/go-ap/activitypub"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
@@ -1072,6 +1073,8 @@ func registerRoutes(se *core.ServeEvent, client meilisearch.ServiceManager) {
|
||||
return e.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||
})
|
||||
|
||||
se.Router.POST("/waypoint/cluster", waypointcluster.Handler)
|
||||
|
||||
se.Router.POST("/auth/token", func(e *core.RequestEvent) error {
|
||||
var data struct {
|
||||
APIToken string `json:"api_token"`
|
||||
@@ -1488,6 +1491,10 @@ func bootstrapCategories(app core.App) error {
|
||||
for _, element := range categories {
|
||||
record := core.NewRecord(collection)
|
||||
record.Set("name", element)
|
||||
record.Set("settings", map[string]any{
|
||||
"wp_merge_enabled": true,
|
||||
"wp_merge_radius": 50,
|
||||
})
|
||||
f, _ := filesystem.NewFileFromPath("migrations/initial_data/" + strings.ToLower(element) + ".jpg")
|
||||
record.Set("img", f)
|
||||
err := app.Save(record)
|
||||
|
||||
60
db/migrations/1763300311_updated_categories.go
Normal file
60
db/migrations/1763300311_updated_categories.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"github.com/pocketbase/pocketbase/core"
|
||||
m "github.com/pocketbase/pocketbase/migrations"
|
||||
)
|
||||
|
||||
func init() {
|
||||
m.Register(func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("kjxvi8asj2igqwf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// add field
|
||||
if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{
|
||||
"hidden": false,
|
||||
"id": "json3846545605",
|
||||
"maxSize": 0,
|
||||
"name": "settings",
|
||||
"presentable": false,
|
||||
"required": false,
|
||||
"system": false,
|
||||
"type": "json"
|
||||
}`)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := app.Save(collection); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
records, err := app.FindAllRecords("categories")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, record := range records {
|
||||
record.Set("settings", map[string]any{
|
||||
"wp_merge_enabled": true,
|
||||
"wp_merge_radius": 50,
|
||||
})
|
||||
if err := app.Save(record); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}, func(app core.App) error {
|
||||
collection, err := app.FindCollectionByNameOrId("kjxvi8asj2igqwf")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// remove field
|
||||
collection.Fields.RemoveById("json3846545605")
|
||||
|
||||
return app.Save(collection)
|
||||
})
|
||||
}
|
||||
14
db/util/geo.go
Normal file
14
db/util/geo.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package util
|
||||
|
||||
import "math"
|
||||
|
||||
func HaversineDistance(lat1 float64, lon1 float64, lat2 float64, lon2 float64) float64 {
|
||||
const earthRadiusKm = 6371
|
||||
dLat := (lat2 - lat1) * (math.Pi / 180)
|
||||
dLon := (lon2 - lon1) * (math.Pi / 180)
|
||||
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
||||
math.Cos(lat1*(math.Pi/180))*math.Cos(lat2*(math.Pi/180))*
|
||||
math.Sin(dLon/2)*math.Sin(dLon/2)
|
||||
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||
return earthRadiusKm * c * 1000
|
||||
}
|
||||
197
db/waypointcluster/waypoint_cluster.go
Normal file
197
db/waypointcluster/waypoint_cluster.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package waypointcluster
|
||||
|
||||
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 Handler(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)
|
||||
}
|
||||
Reference in New Issue
Block a user