Files
wanderer/plugins/komoot/mapper.go
slothful-vassal 485ec53f6d feat: add plugin system (#1034)
* feat: add plugin system

* fix db docker build

* fix hammerhead readme, add strava subscription news to docs

* fixes and sdk improvements

* fix: reduce Meilisearch load, debounce federation sync (#1012)

* optimize meili trail index

* several fixes

---------

Co-authored-by: Flomp <Flomp@users.noreply.github.com>

* Bump svelte from 5.55.5 to 5.56.0 in /docs (#1032)

Bumps [svelte](https://github.com/sveltejs/svelte/tree/HEAD/packages/svelte) from 5.55.5 to 5.56.0.
- [Release notes](https://github.com/sveltejs/svelte/releases)
- [Changelog](https://github.com/sveltejs/svelte/blob/main/packages/svelte/CHANGELOG.md)
- [Commits](https://github.com/sveltejs/svelte/commits/svelte@5.56.0/packages/svelte)

---
updated-dependencies:
- dependency-name: svelte
  dependency-version: 5.56.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>

* Release v0.19.2 (#1035)

* chore: release v0.19.2

* add changelog

---------

Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
Co-authored-by: Christian Beutel <>

* speed up plugin sync and several small fixes

* concepts for security improvements and process stability

* improve concept

* security concept implemented

* remove insecure TLS

* worker concept implemented

* fixes and cleanup

* fixes

* docu

* mermaid, namings

* WASM plugin host improvements, plugin logging

* fix db migration

* Improve plugin config and category mapping UI

* fixes

* further fixes

* remove manual test sync

* fix db migration and strava mapping

* type added, UI improvements

* fix plugin card toggle clickable area

* optimize synch status card layout

* plugin type 'trails' instead of 'integration'

* session auth validation in UI

* fix komoot date and waypoints

* improve category mapping

* fix send to hammerhead: trail name

* plugin setup error handling improved

* fix review findings

* re-mapping added

* rename remote_category

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Flomp <Flomp@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Flomp <26000991+Flomp@users.noreply.github.com>
2026-06-22 15:00:44 +02:00

227 lines
5.6 KiB
Go

package main
import (
"encoding/base64"
"fmt"
"strconv"
"strings"
"time"
sdkgpx "github.com/open-wanderer/wanderer/plugins/sdk/gpx"
)
func tourImport(tour *detailedTour, routeImages []imageItem) (trailImport, error) {
gpxData, err := tourGPX(tour)
if err != nil {
return trailImport{}, err
}
privacy := privacyFromStatus(tour.Status)
return trailImport{
Source: trailImportSource{
Provider: "komoot",
ExternalID: strconv.FormatInt(tour.ID, 10),
},
Kind: kindFromType(tour.Type),
Name: tour.Name,
Description: tour.Description,
StartedAt: tour.Date,
ActivityType: activityType(tour.Sport),
Privacy: &privacy,
Track: track{
Format: "gpx",
ContentBase64: base64.StdEncoding.EncodeToString(gpxData),
},
Waypoints: waypoints(tour),
Photos: photos(tour, routeImages),
Metadata: map[string]any{
"distance": tour.Distance,
"elevationGain": tour.ElevationUp,
"elevationLoss": tour.ElevationDown,
"duration": tour.Duration,
"providerCategory": tour.Sport,
"sourceSport": tour.Sport,
"difficulty": tour.Difficulty.Grade,
},
}, nil
}
func tourGPX(tour *detailedTour) ([]byte, error) {
items := tour.Embedded.Coordinates.Items
points := make([]sdkgpx.Point, 0, len(items))
startedAt, _ := time.Parse(time.RFC3339, tour.Date)
for _, item := range items {
elevation := item.Alt
point := sdkgpx.Point{
Lat: item.Lat,
Lon: item.Lng,
Elevation: &elevation,
}
if !startedAt.IsZero() {
pointTime := startedAt.Add(time.Duration(item.T) * time.Millisecond).UTC()
point.Time = &pointTime
}
points = append(points, point)
}
return sdkgpx.Track("wanderer Komoot plugin", tour.Name, points)
}
func waypoints(tour *detailedTour) []waypoint {
result := make([]waypoint, 0, len(tour.Embedded.WayPoints.Embedded.Items)+len(tour.Embedded.Timeline.Embedded.Items))
seen := map[string]bool{}
result = appendWaypoints(result, seen, tour.Embedded.WayPoints.Embedded.Items)
result = appendWaypoints(result, seen, tour.Embedded.Timeline.Embedded.Items)
return result
}
func appendWaypoints(result []waypoint, seen map[string]bool, items []timelineItem) []waypoint {
for _, item := range items {
ref := item.Embedded.Reference
point, ok := waypointPoint(ref)
if ref.Name == "" || !ok {
continue
}
key := ref.ID.String()
if key == "" {
key = fmt.Sprintf("%s:%0.7f:%0.7f", ref.Name, point.Lat, point.Lng)
}
if seen[key] {
continue
}
seen[key] = true
description := ""
if len(ref.Embedded.Tips.Embedded.Items) > 0 {
description = ref.Embedded.Tips.Embedded.Items[0].Text
}
ele := point.Alt
result = append(result, waypoint{
ExternalID: ref.ID.String(),
Name: ref.Name,
Description: description,
Lat: point.Lat,
Lon: point.Lng,
Ele: &ele,
Icon: "circle",
Photos: waypointPhotos(item),
})
}
return result
}
func waypointPoint(ref waypointReference) (point, bool) {
if ref.StartPoint.Lat != 0 || ref.StartPoint.Lng != 0 {
return ref.StartPoint, true
}
if ref.Location.Lat != 0 || ref.Location.Lng != 0 {
return ref.Location, true
}
return point{}, false
}
func photos(tour *detailedTour, routeImages []imageItem) []photo {
images := routeImages
if len(images) == 0 {
images = tour.Embedded.CoverImages.Embedded.Items
}
if len(images) == 0 && tour.MapImage.Src != "" {
images = []imageItem{{Src: tour.MapImage.Src, Type: "image/jpeg"}}
}
return photosFromImages(images, "komoot-photo.jpg")
}
func waypointPhotos(item timelineItem) []photo {
ref := item.Embedded.Reference
images := ref.Embedded.Images.Embedded.Items
if ref.Embedded.FrontImage.Src != "" {
images = append([]imageItem{ref.Embedded.FrontImage}, images...)
}
return photosFromImages(images, "komoot-waypoint-photo.jpg")
}
func photosFromImages(images []imageItem, fallbackFilename string) []photo {
result := make([]photo, 0, len(images))
seen := map[string]bool{}
for _, image := range images {
source := expandImageURL(image.Src)
if source == "" || strings.HasSuffix(strings.ToLower(source), ".gif") {
continue
}
key := image.ID.String()
if key == "" {
key = source
}
if seen[key] {
continue
}
seen[key] = true
result = append(result, photo{
ExternalID: image.ID.String(),
Filename: filenameForImage(image.ID, fallbackFilename),
ContentType: contentType(image.Type),
Lat: optionalCoordinate(image.Location.Lat),
Lon: optionalCoordinate(image.Location.Lng),
Source: mediaSource{
Type: "url",
URL: source,
},
})
}
return result
}
func expandImageURL(source string) string {
source = strings.ReplaceAll(source, "{crop}", "false")
source = strings.ReplaceAll(source, "{width}", "")
source = strings.ReplaceAll(source, "{height}", "")
return source
}
func filenameForImage(id flexibleID, fallback string) string {
if id.String() == "" {
return fallback
}
return fmt.Sprintf("komoot-%s.jpg", id.String())
}
func contentType(value string) string {
if strings.HasPrefix(value, "image/") {
return value
}
return "image/jpeg"
}
func optionalCoordinate(value float64) *float64 {
if value == 0 {
return nil
}
return &value
}
func kindFromType(value string) string {
if value == "tour_recorded" {
return "completed"
}
return "planned"
}
func privacyFromStatus(value string) string {
if value == "public" {
return "public"
}
return "private"
}
func activityType(sport string) string {
switch sport {
case "hike", "mountaineering":
return "hiking"
case "jogging":
return "running"
case "touringbicycle", "mtb", "racebike", "mtb_easy", "mtb_advanced":
return "biking"
default:
return sport
}
}