* merge trails function added * fix conflict resolve issues * avoid incorrect authorship when merging trail comments * require edit access for all selected trails before merge * fix likes checkbox binding in trail merge modal * improve error handling * require editable target and deletable sources for merge * avoid duplicate trail fetch during merge * prevent duplicate likes when merging trails * check photo download responses during trail merge * add merge completion toast notifications * cleanup * small fixes * move merge code to backend, option to merge single trail selection with similar one, automatic merge for integrations add as option, maintenance page to find and merge similar trails added * fix build error * fix * docu, beautifying, refactoring --------- Co-authored-by: Flomp <Flomp@users.noreply.github.com>
174 lines
4.2 KiB
Go
174 lines
4.2 KiB
Go
package util
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
|
|
"github.com/pocketbase/pocketbase/core"
|
|
"github.com/tkrajina/gpxgo/gpx"
|
|
)
|
|
|
|
type TrailGeometryMetrics struct {
|
|
MeanDistanceMeters float64
|
|
MaxDistanceMeters float64
|
|
StartDistanceMeters float64
|
|
EndDistanceMeters float64
|
|
}
|
|
|
|
type trailPoint struct {
|
|
Lat float64
|
|
Lon float64
|
|
}
|
|
|
|
func TrailCoordinates(app core.App, r *core.Record) ([][2]float64, error) {
|
|
gpxPath := r.GetString("gpx")
|
|
if gpxPath == "" {
|
|
return nil, nil
|
|
}
|
|
|
|
fsys, err := app.NewFilesystem()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer fsys.Close()
|
|
|
|
reader, err := fsys.GetReader(r.BaseFilesPath() + "/" + gpxPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer reader.Close()
|
|
|
|
content := new(bytes.Buffer)
|
|
if _, err := io.Copy(content, reader); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
gpxData, err := gpx.Parse(content)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
points := make([][2]float64, 0)
|
|
for _, trk := range gpxData.Tracks {
|
|
for _, seg := range trk.Segments {
|
|
for _, pt := range seg.Points {
|
|
points = append(points, [2]float64{pt.Latitude, pt.Longitude})
|
|
}
|
|
}
|
|
}
|
|
|
|
return points, nil
|
|
}
|
|
|
|
func TrailGeometrySimilarity(app core.App, a *core.Record, b *core.Record) (*TrailGeometryMetrics, error) {
|
|
aCoords, err := TrailCoordinates(app, a)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load source geometry: %w", err)
|
|
}
|
|
bCoords, err := TrailCoordinates(app, b)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("load target geometry: %w", err)
|
|
}
|
|
|
|
return CompareTrailCoordinates(aCoords, bCoords)
|
|
}
|
|
|
|
func CompareTrailCoordinates(aCoords [][2]float64, bCoords [][2]float64) (*TrailGeometryMetrics, error) {
|
|
if len(aCoords) < 2 || len(bCoords) < 2 {
|
|
return nil, fmt.Errorf("missing geometry")
|
|
}
|
|
|
|
a := resampleTrailCoordinates(aCoords, 64)
|
|
b := resampleTrailCoordinates(bCoords, 64)
|
|
if len(a) < 2 || len(b) < 2 {
|
|
return nil, fmt.Errorf("missing geometry")
|
|
}
|
|
|
|
return compareSampledTrails(a, b), nil
|
|
}
|
|
|
|
func compareSampledTrails(a []trailPoint, b []trailPoint) *TrailGeometryMetrics {
|
|
count := min(len(a), len(b))
|
|
if count == 0 {
|
|
return &TrailGeometryMetrics{}
|
|
}
|
|
|
|
sum := 0.0
|
|
maxDistance := 0.0
|
|
for i := 0; i < count; i++ {
|
|
distance := HaversineDistanceMeters(a[i].Lat, a[i].Lon, b[i].Lat, b[i].Lon)
|
|
sum += distance
|
|
if distance > maxDistance {
|
|
maxDistance = distance
|
|
}
|
|
}
|
|
|
|
return &TrailGeometryMetrics{
|
|
MeanDistanceMeters: sum / float64(count),
|
|
MaxDistanceMeters: maxDistance,
|
|
StartDistanceMeters: HaversineDistanceMeters(a[0].Lat, a[0].Lon, b[0].Lat, b[0].Lon),
|
|
EndDistanceMeters: HaversineDistanceMeters(a[count-1].Lat, a[count-1].Lon, b[count-1].Lat, b[count-1].Lon),
|
|
}
|
|
}
|
|
|
|
func resampleTrailCoordinates(coords [][2]float64, targetPoints int) []trailPoint {
|
|
points := make([]trailPoint, 0, len(coords))
|
|
for _, coord := range coords {
|
|
points = append(points, trailPoint{Lat: coord[0], Lon: coord[1]})
|
|
}
|
|
|
|
if len(points) <= 2 || targetPoints <= 2 {
|
|
return points
|
|
}
|
|
|
|
cumulative := make([]float64, len(points))
|
|
total := 0.0
|
|
for i := 1; i < len(points); i++ {
|
|
total += HaversineDistanceMeters(points[i-1].Lat, points[i-1].Lon, points[i].Lat, points[i].Lon)
|
|
cumulative[i] = total
|
|
}
|
|
|
|
if total == 0 {
|
|
return []trailPoint{points[0], points[len(points)-1]}
|
|
}
|
|
|
|
resampled := make([]trailPoint, 0, targetPoints)
|
|
for i := 0; i < targetPoints; i++ {
|
|
targetDistance := (float64(i) / float64(targetPoints-1)) * total
|
|
resampled = append(resampled, interpolateTrailPoint(points, cumulative, targetDistance))
|
|
}
|
|
|
|
return resampled
|
|
}
|
|
|
|
func interpolateTrailPoint(points []trailPoint, cumulative []float64, targetDistance float64) trailPoint {
|
|
if targetDistance <= 0 {
|
|
return points[0]
|
|
}
|
|
lastIndex := len(points) - 1
|
|
if targetDistance >= cumulative[lastIndex] {
|
|
return points[lastIndex]
|
|
}
|
|
|
|
for i := 1; i < len(points); i++ {
|
|
if cumulative[i] < targetDistance {
|
|
continue
|
|
}
|
|
|
|
prevDistance := cumulative[i-1]
|
|
nextDistance := cumulative[i]
|
|
if nextDistance == prevDistance {
|
|
return points[i]
|
|
}
|
|
|
|
ratio := (targetDistance - prevDistance) / (nextDistance - prevDistance)
|
|
return trailPoint{
|
|
Lat: points[i-1].Lat + (points[i].Lat-points[i-1].Lat)*ratio,
|
|
Lon: points[i-1].Lon + (points[i].Lon-points[i-1].Lon)*ratio,
|
|
}
|
|
}
|
|
|
|
return points[lastIndex]
|
|
}
|