├── .env.sample ├── .gitignore ├── komoot ├── constants.go ├── requests.go ├── client.go ├── coordinates.go ├── responses.go ├── tours.go ├── model.go └── upload.go ├── .vscode ├── settings.json └── tasks.json ├── .github └── dependabot.yml ├── go.mod ├── internal.go ├── Makefile ├── main.go ├── README.md ├── go.sum └── data └── test.gpx /.env.sample: -------------------------------------------------------------------------------- 1 | KOMOOT_EMAIL=user@host.com 2 | KOMOOT_PASSWD=password 3 | KOMOOT_USER_ID=123456 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | export-komoot 3 | export 4 | .DS_Store 5 | export-komoot*.tar.gz 6 | export-komoot*.zip 7 | .idea/ 8 | -------------------------------------------------------------------------------- /komoot/constants.go: -------------------------------------------------------------------------------- 1 | package komoot 2 | 3 | const contentTypeJson = "application/json" 4 | const acceptJson = "application/hal+json,application/json" 5 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.testFlags": [ 3 | "-nocolor" 4 | ], 5 | "files.exclude": { 6 | "**/.git": true, 7 | "**/.svn": true, 8 | "**/.hg": true, 9 | "**/CVS": true, 10 | "**/.DS_Store": true, 11 | "export-komoot": true, 12 | "export": true, 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "gomod" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /komoot/requests.go: -------------------------------------------------------------------------------- 1 | package komoot 2 | 3 | type RoutePlanRequestPath struct { 4 | Reference string `json:"reference,omitempty"` 5 | Location Coordinate `json:"location"` 6 | } 7 | 8 | type RoutePlanRequestSegment struct { 9 | Type string `json:"type,omitempty"` 10 | } 11 | 12 | type RoutePlanRequest struct { 13 | Constitution int64 `json:"constitution"` 14 | Path []RoutePlanRequestPath `json:"path"` 15 | Segments []RoutePlanRequestSegment `json:"segments"` 16 | Sport string `json:"sport"` 17 | } 18 | -------------------------------------------------------------------------------- /komoot/client.go: -------------------------------------------------------------------------------- 1 | package komoot 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // See: https://github.com/janthomas89/komoot-api-client/blob/master/src/KomootApiClient.php 8 | // See: https://static.komoot.de/doc/auth/oauth2.html 9 | 10 | type Client struct { 11 | Email string 12 | Password string 13 | UserID int64 14 | IsLoggedIn bool 15 | komootDomain string 16 | httpClient *http.Client 17 | } 18 | 19 | func NewClient(email string, password string, userID int64) *Client { 20 | return &Client{ 21 | Email: email, 22 | Password: password, 23 | UserID: userID, 24 | IsLoggedIn: false, 25 | komootDomain: ".komoot.com", 26 | httpClient: &http.Client{ 27 | CheckRedirect: nil, 28 | }, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pieterclaerhout/export-komoot 2 | 3 | go 1.24 4 | 5 | require ( 6 | github.com/alexflint/go-arg v1.6.0 7 | github.com/gosimple/slug v1.15.0 8 | github.com/joho/godotenv v1.5.1 9 | github.com/pieterclaerhout/go-log v1.15.0 10 | github.com/pieterclaerhout/go-waitgroup v1.0.7 11 | ) 12 | 13 | require ( 14 | github.com/alexflint/go-scalar v1.2.0 // indirect 15 | github.com/fatih/color v1.18.0 // indirect 16 | github.com/gosimple/unidecode v1.0.1 // indirect 17 | github.com/mattn/go-colorable v0.1.14 // indirect 18 | github.com/mattn/go-isatty v0.0.20 // indirect 19 | github.com/pkg/errors v0.9.1 // indirect 20 | github.com/rotisserie/eris v0.5.4 // indirect 21 | github.com/sanity-io/litter v1.5.8 // indirect 22 | golang.org/x/sys v0.30.0 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /internal.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "os" 7 | 8 | "github.com/pieterclaerhout/export-komoot/komoot" 9 | ) 10 | 11 | func fileExists(path string) bool { 12 | _, err := os.Stat(path) 13 | return err == nil 14 | } 15 | 16 | func formatJSON(data []byte) []byte { 17 | var out bytes.Buffer 18 | err := json.Indent(&out, data, "", "\t") 19 | if err != nil { 20 | return data 21 | } 22 | return out.Bytes() 23 | } 24 | 25 | func saveFormattedJSON(data []byte, path string) error { 26 | data = formatJSON(data) 27 | return os.WriteFile(path, data, 0755) 28 | } 29 | 30 | func saveTourFile(data []byte, path string, tour komoot.Tour) error { 31 | if err := os.WriteFile(path, data, 0755); err != nil { 32 | return err 33 | } 34 | 35 | os.Chtimes(path, tour.Date, tour.ChangedAt) 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /komoot/coordinates.go: -------------------------------------------------------------------------------- 1 | package komoot 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | ) 10 | 11 | func (client *Client) Coordinates(tour Tour) (*CoordinatesResponse, error) { 12 | 13 | downloadURL := fmt.Sprintf("https://www%s/api/v007/tours/%d/coordinates", client.komootDomain, tour.ID) 14 | 15 | req, err := http.NewRequest(http.MethodGet, downloadURL, nil) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | req.Header.Add("Authorization", "Basic "+client.basicAuth()) 21 | 22 | resp, err := client.httpClient.Do(req) 23 | if err != nil { 24 | return nil, err 25 | } 26 | defer resp.Body.Close() 27 | 28 | if resp.StatusCode != http.StatusOK { 29 | return nil, errors.New(resp.Status) 30 | } 31 | 32 | body, err := io.ReadAll(resp.Body) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | var r CoordinatesResponse 38 | if err := json.Unmarshal(body, &r); err != nil { 39 | return nil, err 40 | } 41 | 42 | r.Tour = &tour 43 | 44 | return &r, nil 45 | 46 | } 47 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "export-komoot | tests", 8 | "type": "shell", 9 | "command": "make test", 10 | "problemMatcher": [ 11 | "$go" 12 | ] 13 | }, 14 | { 15 | "label": "export-komoot | build", 16 | "type": "shell", 17 | "command": "make build", 18 | "problemMatcher": [ 19 | "$go" 20 | ] 21 | }, 22 | { 23 | "label": "export-komoot | run-incremental", 24 | "type": "shell", 25 | "command": "make run-incremental", 26 | "problemMatcher": [ 27 | "$go" 28 | ] 29 | }, 30 | { 31 | "label": "export-komoot | run-full", 32 | "type": "shell", 33 | "command": "make run-full", 34 | "problemMatcher": [ 35 | "$go" 36 | ] 37 | }, 38 | { 39 | "label": "export-komoot | help", 40 | "type": "shell", 41 | "command": "make help", 42 | "problemMatcher": [ 43 | "$go" 44 | ] 45 | }, 46 | { 47 | "label": "export-komoot | run-filter", 48 | "type": "shell", 49 | "command": "make run-filter", 50 | "problemMatcher": [ 51 | "$go" 52 | ] 53 | } 54 | ] 55 | } 56 | -------------------------------------------------------------------------------- /komoot/responses.go: -------------------------------------------------------------------------------- 1 | package komoot 2 | 3 | type ErrorResponse struct { 4 | Error string `json:"error"` 5 | Code int `json:"code"` 6 | Message string `json:"message"` 7 | } 8 | 9 | type CoordinatesResponse struct { 10 | Tour *Tour `json:"-"` 11 | Items []Coordinate `json:"items"` 12 | } 13 | 14 | type ToursResponse struct { 15 | Embedded struct { 16 | Tours []Tour `json:"tours"` 17 | } `json:"_embedded"` 18 | } 19 | 20 | type UploadTourResponse struct { 21 | Links struct { 22 | Self struct { 23 | Href string `json:"href"` 24 | } `json:"self"` 25 | } `json:"_links"` 26 | Embedded struct { 27 | Items []UploadedTour `json:"items"` 28 | Matched MatchedTour `json:"matched"` 29 | } `json:"_embedded"` 30 | Message string `json:"message"` 31 | } 32 | 33 | type MatchedTour struct { 34 | Constitution int64 `json:"constitution"` 35 | Status string `json:"status"` 36 | Date string `json:"date"` 37 | Difficulty Difficulty `json:"difficulty"` 38 | Distance float64 `json:"distance"` 39 | Duration float64 `json:"duration"` 40 | ElevationDown float64 `json:"elevation_down"` 41 | ElevationUp float64 `json:"elevation_up"` 42 | Name string `json:"name"` 43 | Path []Path `json:"path"` 44 | Query string `json:"query"` 45 | Segments []Segment `json:"segments"` 46 | Source string `json:"source"` 47 | Sport string `json:"sport"` 48 | Summary struct { 49 | Surfaces []Surface `json:"surfaces"` 50 | WayTypes []WayType `json:"way_types"` 51 | } `json:"summary"` 52 | TourInformation []TourInformation `json:"tour_information"` 53 | Type string `json:"type"` 54 | Embedded struct { 55 | Coordinates struct { 56 | Items []Coordinate `json:"items"` 57 | } `json:"coordinates"` 58 | Directions struct { 59 | Items []Direction `json:"items"` 60 | } `json:"directions"` 61 | Surfaces struct { 62 | Items []EmbeddedSurface `json:"items"` 63 | } `json:"surfaces"` 64 | WayTypes struct { 65 | Items []EmbeddedWayType `json:"items"` 66 | } `json:"way_types"` 67 | } `json:"_embedded"` 68 | } 69 | 70 | type RoutePlanResponse struct { 71 | Duration float64 `json:"duration"` 72 | } 73 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | export 3 | 4 | APP_NAME := export-komoot 5 | REVISION := $(shell git rev-parse --short HEAD) 6 | BRANCH := $(shell git rev-parse --abbrev-ref HEAD | tr -d '\040\011\012\015\n') 7 | BUILD_CMD := go build -buildvcs -ldflags "-s -w -X $(PACKAGE).GitRevision=$(REVISION) -X $(PACKAGE).GitBranch=$(BRANCH)" -trimpath 8 | 9 | init: 10 | @go mod download 11 | 12 | build: init 13 | $(call build-binary) 14 | 15 | test: 16 | @go test -cover `go list ./... | grep -v cmd` 17 | 18 | run-incremental: build 19 | @DEBUG=0 ./$(APP_NAME) --email "$(KOMOOT_EMAIL)" --password "$(KOMOOT_PASSWD)" --userid "$(KOMOOT_USER_ID)" --to "export" 20 | 21 | run-full: build 22 | @DEBUG=0 ./$(APP_NAME) --email "$(KOMOOT_EMAIL)" --password "$(KOMOOT_PASSWD)" --userid "$(KOMOOT_USER_ID)" --to "export" --fulldownload 23 | 24 | run-filter: build 25 | @DEBUG=0 ./$(APP_NAME) --email "$(KOMOOT_EMAIL)" --password "$(KOMOOT_PASSWD)" --userid "$(KOMOOT_USER_ID)" --to "export" --fulldownload --filter "*KK*" 26 | 27 | help: build 28 | @DEBUG=0 ./$(APP_NAME) --help 29 | 30 | build-all: 31 | @rm -f ./$(APP_NAME)-* 32 | $(call build-binary-mac) 33 | $(call build-binary-linux) 34 | $(call build-binary-windows) 35 | 36 | ## Helper functions 37 | define build-binary 38 | @GOOS=$(1) GOARCH=$(2) $(BUILD_CMD) -o $(APP_NAME) 39 | endef 40 | 41 | define build-binary-mac 42 | @echo "Building $(APP_NAME) for macos" 43 | $(call build-binary,darwin,arm64,arm64) 44 | @mv $(APP_NAME) $(APP_NAME)-arm 45 | $(call build-binary,darwin,amd64,x86_64) 46 | @mv $(APP_NAME) $(APP_NAME)-x86 47 | @lipo -create -output $(APP_NAME) $(APP_NAME)-x86 $(APP_NAME)-arm 48 | @tar czf ./$(APP_NAME)-$(REVISION)-macos.tar.gz $(APP_NAME) 49 | @rm -f $(APP_NAME)-x86 $(APP_NAME)-arm $(APP_NAME) 50 | endef 51 | 52 | define build-binary-linux 53 | @echo "Building $(APP_NAME) for linux" 54 | $(call build-binary,linux,arm64,arm64) 55 | @tar czf ./$(APP_NAME)-$(REVISION)-linux-arm64.tar.gz $(APP_NAME) 56 | $(call build-binary,linux,amd64,x86_64) 57 | @tar czf ./$(APP_NAME)-$(REVISION)-linux-x86_64.tar.gz $(APP_NAME) 58 | @rm -f $(APP_NAME) 59 | endef 60 | 61 | define build-binary-windows 62 | @echo "Building $(APP_NAME) for windows" 63 | @$(call build-binary,windows,arm64,arm64) 64 | @mv ./$(APP_NAME) ./$(APP_NAME).exe 65 | @zip -rq ./$(APP_NAME)-$(REVISION)-windows-arm64.zip $(APP_NAME).exe 66 | @$(call build-binary,windows,amd64,x86_64) 67 | @mv ./$(APP_NAME) ./$(APP_NAME).exe 68 | @zip -rq ./$(APP_NAME)-$(REVISION)-windows-x86_64.zip $(APP_NAME).exe 69 | @rm -f $(APP_NAME).exe $(APP_NAME) 70 | endef 71 | -------------------------------------------------------------------------------- /komoot/tours.go: -------------------------------------------------------------------------------- 1 | package komoot 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | ) 11 | 12 | func (client *Client) Tours(filter string, tourType string) ([]Tour, []byte, error) { 13 | params := url.Values{} 14 | params.Set("limit", "5000") 15 | params.Set("sport_types", tourType) 16 | params.Set("status", "private") 17 | params.Set("name", filter) 18 | 19 | req, err := http.NewRequest( 20 | http.MethodGet, 21 | fmt.Sprintf("https://www%s/api/v007/users/%d/tours/?%s", client.komootDomain, client.UserID, params.Encode()), 22 | nil, 23 | ) 24 | if err != nil { 25 | return nil, nil, err 26 | } 27 | req.Header.Set("Accept", acceptJson) 28 | req.Header.Add("Authorization", "Basic "+client.basicAuth()) 29 | 30 | resp, err := client.httpClient.Do(req) 31 | if err != nil { 32 | return nil, nil, err 33 | } 34 | defer resp.Body.Close() 35 | 36 | body, err := io.ReadAll(resp.Body) 37 | if err != nil { 38 | return nil, nil, err 39 | } 40 | 41 | if err := client.checkError(resp, body); err != nil { 42 | return nil, nil, err 43 | } 44 | 45 | var r ToursResponse 46 | if err := json.Unmarshal(body, &r); err != nil { 47 | return nil, nil, err 48 | } 49 | 50 | return r.Embedded.Tours, body, nil 51 | } 52 | 53 | func (client *Client) Download(tour Tour) ([]byte, error) { 54 | req, err := http.NewRequest( 55 | http.MethodGet, 56 | fmt.Sprintf("https://www%s/api/v007/tours/%d.gpx", client.komootDomain, tour.ID), 57 | nil, 58 | ) 59 | if err != nil { 60 | return nil, err 61 | } 62 | req.Header.Set("Accept", acceptJson) 63 | req.Header.Add("Authorization", "Basic "+client.basicAuth()) 64 | 65 | resp, err := client.httpClient.Do(req) 66 | if err != nil { 67 | return nil, err 68 | } 69 | defer resp.Body.Close() 70 | 71 | body, err := io.ReadAll(resp.Body) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | if err := client.checkError(resp, body); err != nil { 77 | return nil, err 78 | } 79 | 80 | return body, nil 81 | } 82 | 83 | func (client *Client) basicAuth() string { 84 | auth := client.Email + ":" + client.Password 85 | return base64.StdEncoding.EncodeToString([]byte(auth)) 86 | } 87 | 88 | func (client *Client) checkError(resp *http.Response, body []byte) error { 89 | if resp.StatusCode == http.StatusOK { 90 | return nil 91 | } 92 | 93 | var r ErrorResponse 94 | if err := json.Unmarshal(body, &r); err != nil { 95 | return err 96 | } 97 | 98 | return fmt.Errorf("%s (%d %s)", r.Message, r.Code, r.Error) 99 | } 100 | -------------------------------------------------------------------------------- /komoot/model.go: -------------------------------------------------------------------------------- 1 | package komoot 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/gosimple/slug" 8 | ) 9 | 10 | type UploadedTour struct { 11 | Type string `json:"type"` 12 | Source string `json:"source"` 13 | Sport string `json:"sport"` 14 | Constitution int64 `json:"constitution"` 15 | Name string `json:"name"` 16 | Date time.Time `json:"date"` 17 | Embedded struct { 18 | Coordinates struct { 19 | Items []Coordinate `json:"items"` 20 | } `json:"coordinates"` 21 | } `json:"_embedded"` 22 | } 23 | 24 | type Coordinate struct { 25 | Lat float64 `json:"lat"` 26 | Lng float64 `json:"lng"` 27 | Alt float64 `json:"alt"` 28 | T int64 `json:"t"` 29 | } 30 | 31 | type Difficulty struct { 32 | ExplanationFitness string `json:"explanation_fitness"` 33 | ExplanationTechnical string `json:"explanation_technical"` 34 | Grade string `json:"grade"` 35 | } 36 | 37 | type Path struct { 38 | Index int64 `json:"index"` 39 | Reference string `json:"reference,omitempty"` 40 | Location Coordinate `json:"location"` 41 | } 42 | 43 | type Segment struct { 44 | From int64 `json:"from"` 45 | To int64 `json:"to"` 46 | Type string `json:"type,omitempty"` 47 | } 48 | 49 | type Direction struct { 50 | CardinalDirection string `json:"cardinal_direction"` 51 | ChangeWay bool `json:"change_way"` 52 | Complex bool `json:"complex"` 53 | Distance int64 `json:"distance"` 54 | Index int64 `json:"index"` 55 | LastSimilar int64 `json:"last_similar"` 56 | StreetName string `json:"street_name"` 57 | Type string `json:"type"` 58 | WayType string `json:"way_type"` 59 | } 60 | 61 | type Surface struct { 62 | Amount float64 `json:"amount"` 63 | Type string `json:"type"` 64 | } 65 | 66 | func (surface Surface) String() string { 67 | switch surface.Type { 68 | case "sb#unpaved": 69 | return "Unpaved" 70 | case "sb#cobbles": 71 | return "Cobbles" 72 | case "sb#paved": 73 | return "Paved" 74 | case "sb#asphalt": 75 | return "Asphalt" 76 | case "sf#unknown": 77 | return "Unknown" 78 | case "sb#compacted": 79 | return "Gravel" 80 | default: 81 | return surface.Type 82 | } 83 | } 84 | 85 | type EmbeddedSurface struct { 86 | From int64 `json:"from"` 87 | To int64 `json:"to"` 88 | Element string `json:"element"` 89 | } 90 | 91 | type WayType struct { 92 | Amount float64 `json:"amount"` 93 | Type string `json:"type"` 94 | } 95 | 96 | type EmbeddedWayType struct { 97 | From int64 `json:"from"` 98 | To int64 `json:"to"` 99 | Element string `json:"element"` 100 | } 101 | 102 | type TourInformation struct { 103 | Type string `json:"type"` 104 | Segments []Segment `json:"segments"` 105 | } 106 | 107 | type Tour struct { 108 | ID int64 `json:"id"` 109 | Type string `json:"type"` 110 | Name string `json:"name"` 111 | Sport string `json:"sport"` 112 | Status string `json:"status"` 113 | Date time.Time `json:"date"` 114 | Distance float64 `json:"distance"` 115 | Duration int64 `json:"duration"` 116 | ElevationUp float64 `json:"elevation_up"` 117 | ElevationDown float64 `json:"elevation_down"` 118 | ChangedAt time.Time `json:"changed_at"` 119 | } 120 | 121 | func (tour *Tour) Filename() string { 122 | return fmt.Sprintf( 123 | "%s_%d_%s_%s_%d.gpx", 124 | tour.Date.Format("2006-01-02"), 125 | tour.ID, 126 | slug.Make(tour.Name), 127 | // tour.FormattedSport(), 128 | tour.Type, 129 | tour.ChangedAt.Unix(), 130 | ) 131 | } 132 | 133 | func (tour *Tour) FormattedDistance() string { 134 | return fmt.Sprintf("%.2f km", tour.Distance/1000) 135 | } 136 | 137 | func (tour *Tour) FormattedSport() string { 138 | switch tour.Sport { 139 | case "mtb": 140 | return "mountainbike" 141 | case "racebike": 142 | return "racebike" 143 | case "touringbicycle": 144 | return "touring" 145 | case "mtb_easy": 146 | return "gravel" 147 | case "jogging": 148 | return "running" 149 | default: 150 | return tour.Sport 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "github.com/joho/godotenv" 8 | "os" 9 | "path/filepath" 10 | "time" 11 | 12 | "github.com/alexflint/go-arg" 13 | "github.com/pieterclaerhout/export-komoot/komoot" 14 | "github.com/pieterclaerhout/go-log" 15 | "github.com/pieterclaerhout/go-waitgroup" 16 | ) 17 | 18 | type args struct { 19 | Email string `arg:"env:KOMOOT_EMAIL,required" help:"Your Komoot email address"` 20 | Password string `arg:"env:KOMOOT_PASSWD,required" help:"Your Komoot password"` 21 | UserID int64 `arg:"env:KOMOOT_USER_ID,required" help:"Your Komoot user ID"` 22 | Filter string `help:"Filter tours with name matching this pattern"` 23 | To string `arg:"required" help:"The path to export to"` 24 | FullDownload bool `help:"If specified, all data is redownloaded" default:"false"` 25 | Concurrency int `help:"The number of simultaneous downloads" default:"16"` 26 | TourType string `help:"The type of tours to download" default:""` 27 | } 28 | 29 | func main() { 30 | if _, err := os.Stat(".env"); err == nil { 31 | if err := godotenv.Load(); err != nil { 32 | log.Fatal("Error loading .env file:", err.Error()) 33 | } 34 | } 35 | 36 | var args args 37 | p := arg.MustParse(&args) 38 | 39 | if args.To == "" { 40 | p.Fail("you must provide a value for --to") 41 | } 42 | 43 | log.PrintTimestamp = true 44 | log.PrintColors = true 45 | 46 | start := time.Now() 47 | defer func() { log.Info("Elapsed:", time.Since(start)) }() 48 | 49 | client := komoot.NewClient(args.Email, args.Password, args.UserID) 50 | 51 | fullDstPath, _ := filepath.Abs(args.To) 52 | log.Info("Exporting:", args.Email) 53 | log.Info(" to:", fullDstPath) 54 | 55 | err := os.MkdirAll(args.To, 0777) 56 | log.CheckError(err) 57 | 58 | log.Info("Komoot User ID:", args.UserID) 59 | 60 | tours, resp, err := client.Tours(args.Filter, args.TourType) 61 | log.CheckError(err) 62 | 63 | if len(tours) == 0 { 64 | log.Info("No tours need to be downloaded") 65 | return 66 | } 67 | 68 | log.Info("Found", len(tours), "planned tours") 69 | 70 | var allTours []komoot.Tour 71 | 72 | if !args.FullDownload { 73 | 74 | log.Info("Incremental download, checking what has changed") 75 | 76 | var changedTours []komoot.Tour 77 | 78 | for _, tour := range tours { 79 | 80 | allTours = append(allTours, tour) 81 | 82 | dstPath := filepath.Join(args.To, tour.Filename()) 83 | if !fileExists(dstPath) { 84 | changedTours = append(changedTours, tour) 85 | } 86 | 87 | } 88 | 89 | tours = changedTours 90 | 91 | if len(tours) == 0 { 92 | log.Info("No tours need to be downloaded") 93 | } else { 94 | log.Info("Found", len(tours), "which need to be downloaded") 95 | } 96 | 97 | } else { 98 | allTours = tours 99 | } 100 | 101 | if len(tours) > 0 { 102 | log.Info("Downloading with a concurrency of", args.Concurrency) 103 | wg := waitgroup.NewWaitGroup(args.Concurrency) 104 | 105 | var downloadCount int 106 | 107 | for _, tour := range tours { 108 | 109 | tourToDownload := tour 110 | label := fmt.Sprintf("%10d | %-15s | %-15s | %s", tour.ID, tour.FormattedSport(), tour.Type, tour.Name) 111 | 112 | wg.Add(func() { 113 | 114 | if err := func() error { 115 | 116 | out, err := client.Download(tour) 117 | if err != nil { 118 | return err 119 | } 120 | 121 | dstPath := filepath.Join(args.To, tourToDownload.Filename()) 122 | if err = saveTourFile(out, dstPath, tourToDownload); err != nil { 123 | return err 124 | } 125 | 126 | log.Info("Downloaded:", label) 127 | 128 | return nil 129 | 130 | }(); err != nil { 131 | log.Error("Downloaded:", label, "|", err) 132 | } 133 | downloadCount++ 134 | 135 | }) 136 | 137 | } 138 | 139 | wg.Wait() 140 | 141 | log.Info("Downloaded", downloadCount, "tours") 142 | } 143 | 144 | allTourNames := map[string]bool{} 145 | for _, tour := range allTours { 146 | allTourNames[tour.Filename()] = true 147 | } 148 | 149 | items, err := filepath.Glob(filepath.Join(args.To, "*.gpx")) 150 | log.CheckError(err) 151 | for _, item := range items { 152 | if _, exists := allTourNames[filepath.Base(item)]; exists { 153 | continue 154 | } 155 | log.Info("Deleting:", filepath.Base(item)) 156 | if err := os.Remove(item); err != nil { 157 | log.Warn("Failed to delete:", item, "|", err) 158 | } 159 | } 160 | 161 | log.Info("Saving tour list") 162 | dstPath := filepath.Join(args.To, "tours.json") 163 | err = saveFormattedJSON(resp, dstPath) 164 | log.CheckError(err) 165 | 166 | var out bytes.Buffer 167 | err = json.NewEncoder(&out).Encode(allTours) 168 | log.CheckError(err) 169 | 170 | log.Info("Saving parsed tour list") 171 | dstPath = filepath.Join(args.To, "tours_parsed.json") 172 | err = saveFormattedJSON(out.Bytes(), dstPath) 173 | log.CheckError(err) 174 | 175 | } 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # export-komoot 2 | 3 | This is a tool which allows you to export your planned and recorded tours from [Komoot](https://www.komoot.com). 4 | 5 | > [!NOTE] 6 | > This is an unofficial tool which uses private API's from Komoot and can break at any time… 7 | 8 | # Installing 9 | 10 | To install, you can download the latest release from [the releases](https://github.com/pieterclaerhout/export-komoot/releases). 11 | 12 | > [!NOTE] 13 | > You don't need to have Goland installed when you download the binaries from the release page. 14 | 15 | # Finding your Komoot User ID 16 | 17 | To find your Komoot user ID, login to Komoot, click on your user name in the upper right corner of the screen and 18 | select the option "Profile". The URL you will navigate to will look like this: 19 | 20 | ``` 21 | https://www.komoot.com/>-/user/ 22 | ``` 23 | 24 | Your user ID is the number in the last part of the URL. 25 | 26 | # Usage 27 | 28 | ## Getting the help info 29 | 30 | ``` 31 | $ ./export-komoot -h 32 | Usage: export-komoot --email EMAIL --password PASSWORD --userid USERID [--filter FILTER] --to TO [--fulldownload] [--concurrency CONCURRENCY] [--tourtype TOURTYPE] 33 | 34 | Options: 35 | --email EMAIL Your Komoot email address [env: KOMOOT_EMAIL] 36 | --password PASSWORD Your Komoot password [env: KOMOOT_PASSWD] 37 | --userid USERID Your Komoot user ID [env: KOMOOT_USER_ID] 38 | --filter FILTER Filter tours with name matching this pattern 39 | --to TO The path to export to 40 | --fulldownload If specified, all data is redownloaded [default: false] 41 | --concurrency CONCURRENCY 42 | The number of simultaneous downloads [default: 16] 43 | --tourtype TOURTYPE The type of tours to download 44 | --help, -h display this help and exit 45 | ``` 46 | 47 | ## Running a full export 48 | 49 | To download all planned and recorded tours, you can run: 50 | 51 | ``` 52 | ./export-komoot --email "" --password "" --userid "" --to "" --fulldownload 53 | ``` 54 | 55 | This will download all tours, even if they already exist in the target location. 56 | 57 | ## Running an incremental export (the default) 58 | 59 | To only download the tours which aren't downloaded yet or those that were updated, you can run it like this: 60 | 61 | ``` 62 | ./export-komoot --email "" --password "" --userid "" --to "" 63 | ``` 64 | 65 | ## Filtering the list of tours 66 | 67 | To add a filter to the list of tours that need to be exported, you can use the `--filter` parameter. The filter works 68 | the same way as the search field in the Komoot user interface. 69 | 70 | ``` 71 | ./export-komoot --email "" --password "" --userid "" --to "" --filter "" 72 | ``` 73 | 74 | ## Using a `.env` file 75 | 76 | To avoid that you always have to specify the email, password and user ID, you can store them in a `.env` file. 77 | 78 | Create a `.env` file which should include your username, password and user ID in the current working directory: 79 | 80 | ```env 81 | KOMOOT_EMAIL=user@host.com 82 | KOMOOT_PASSWD=password 83 | KOMOOT_USER_ID=123456 84 | ``` 85 | 86 | Once this is set, you can omit the following parameters from the command: 87 | 88 | - `--email ""` 89 | - `--password ""` 90 | - `--userid ""` 91 | 92 | # About the generated filenames 93 | 94 | The generated filenames use the following structure: 95 | 96 | ``` 97 | ____.gpx 98 | ``` 99 | 100 | - ``: the date of the tour in the format `YYYM-MM-DD` 101 | - ``: the unique ID of the tour in Komoot 102 | - ``: the name of the tour in a filesystem friendly way 103 | - ``: the type of the tour (`tour_planned` or `tour_recorded`) 104 | - ``: the last changed datetime of the tour as a unix timestamp (needed for the incremental export) 105 | 106 | # Limitations 107 | 108 | The tool will only export the first 5000 tours at the moment. 109 | 110 | # Building 111 | 112 | If you prefer to compile the binaries yourself, you will need to have [Golang version 1.24](https://go.dev) or higher 113 | installed. To make building easier, you also need to have the [`make`](https://www.gnu.org/software/make/) utility 114 | installed. Once installed, you can build the binary for the platform you are running using: 115 | 116 | ``` 117 | make build 118 | ``` 119 | 120 | If you want to compile all supported architectures and operating systems, you can execute: 121 | 122 | ``` 123 | make build-all 124 | ``` 125 | 126 | To run the tests, you can execute: 127 | 128 | ``` 129 | make test 130 | ``` 131 | 132 | # References 133 | 134 | - [The new Komoot authentication](https://github.com/Woeler/komoot-php/commit/21065fcf517cc0fac646a6a216b5cf2d851f7975#diff-17339dceedd73393b090f1db8e636e6a8a5a161944c87d85dcd8ec3789dd6112) 135 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alexflint/go-arg v1.6.0 h1:wPP9TwTPO54fUVQl4nZoxbFfKCcy5E6HBCumj1XVRSo= 2 | github.com/alexflint/go-arg v1.6.0/go.mod h1:A7vTJzvjoaSTypg4biM5uYNTkJ27SkNTArtYXnlqVO8= 3 | github.com/alexflint/go-scalar v1.2.0 h1:WR7JPKkeNpnYIOfHRa7ivM21aWAdHD0gEWHCx+WQBRw= 4 | github.com/alexflint/go-scalar v1.2.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o= 5 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 6 | github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 9 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 10 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 11 | github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= 12 | github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= 13 | github.com/gosimple/slug v1.15.0 h1:wRZHsRrRcs6b0XnxMUBM6WK1U1Vg5B0R7VkIf1Xzobo= 14 | github.com/gosimple/slug v1.15.0/go.mod h1:UiRaFH+GEilHstLUmcBgWcI42viBN7mAb818JrYOeFQ= 15 | github.com/gosimple/unidecode v1.0.1 h1:hZzFTMMqSswvf0LBJZCZgThIZrpDHFXux9KeGmn6T/o= 16 | github.com/gosimple/unidecode v1.0.1/go.mod h1:CP0Cr1Y1kogOtx0bJblKzsVWrqYaqfNOnHzpgWw4Awc= 17 | github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= 18 | github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 19 | github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= 20 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 21 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 22 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 23 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 24 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 25 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 26 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 27 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 28 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 29 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 30 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 31 | github.com/pieterclaerhout/go-log v1.15.0 h1:iy9Q+lxGBxRw2BlAjA4Y7WiPmm8N/QZKdjdt72F7MSk= 32 | github.com/pieterclaerhout/go-log v1.15.0/go.mod h1:sE+9p2it5elXm/MOHbdIMTjoO10RPIbwOIZeV2s9K0A= 33 | github.com/pieterclaerhout/go-waitgroup v1.0.7 h1:W9zHJzXO3SPo7Rb3E8hkk8D0gJPdP6DWENt6M08IxWE= 34 | github.com/pieterclaerhout/go-waitgroup v1.0.7/go.mod h1:TJIzZKRP4sc5AT8M0RVUwXhw/OWMxg/lW8VsQz4SRBo= 35 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 36 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 37 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 38 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 39 | github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 40 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 41 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 42 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 43 | github.com/rotisserie/eris v0.5.4 h1:Il6IvLdAapsMhvuOahHWiBnl1G++Q0/L5UIkI5mARSk= 44 | github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9G1A4g9s= 45 | github.com/sanity-io/litter v1.5.5/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= 46 | github.com/sanity-io/litter v1.5.8 h1:uM/2lKrWdGbRXDrIq08Lh9XtVYoeGtcQxk9rtQ7+rYg= 47 | github.com/sanity-io/litter v1.5.8/go.mod h1:9gzJgR2i4ZpjZHsKvUXIRQVk7P+yM3e+jAF7bU2UI5U= 48 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 49 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 50 | github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 51 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 52 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 53 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 54 | github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= 55 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 56 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 57 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 58 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 59 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 60 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 61 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 62 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 63 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 64 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 65 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 66 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 67 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 68 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 69 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 70 | -------------------------------------------------------------------------------- /komoot/upload.go: -------------------------------------------------------------------------------- 1 | package komoot 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | "net/url" 11 | 12 | "github.com/pieterclaerhout/go-log" 13 | ) 14 | 15 | func (client *Client) Upload(name string, gpxData string, sport string, makeRoundtrip bool, overwrite bool) (*MatchedTour, error) { 16 | if overwrite { 17 | existingTours, _, err := client.Tours(name, "") 18 | if err != nil { 19 | return nil, err 20 | } 21 | 22 | if len(existingTours) == 1 { 23 | log.Warn("Overwriting existing tour:", name) 24 | client.deleteTour(existingTours[0]) 25 | } 26 | } 27 | 28 | importedGpx, err := client.importGpx(gpxData) 29 | if err != nil { 30 | return nil, err 31 | } 32 | 33 | tour, err := client.importTour(importedGpx, sport) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | duration, err := client.planRoute(tour) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | tour = client.updateTourSettings(tour, name, sport, makeRoundtrip, duration) 44 | 45 | if err := client.createTour(tour); err != nil { 46 | return nil, err 47 | } 48 | return tour, nil 49 | } 50 | 51 | func (client *Client) importGpx(gpxData string) (*UploadedTour, error) { 52 | params := url.Values{} 53 | params.Set("data_type", "gpx") 54 | 55 | req, err := http.NewRequest( 56 | http.MethodPost, 57 | fmt.Sprintf("https://www%s/api/routing/import/files/?%s", client.komootDomain, params.Encode()), bytes.NewBuffer([]byte(gpxData)), 58 | ) 59 | if err != nil { 60 | return nil, err 61 | } 62 | req.Header.Set("Accept", acceptJson) 63 | req.Header.Add("Authorization", "Basic "+client.basicAuth()) 64 | 65 | resp, err := client.httpClient.Do(req) 66 | if err != nil { 67 | return nil, err 68 | } 69 | defer resp.Body.Close() 70 | 71 | body, err := io.ReadAll(resp.Body) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | log.DebugSeparator("import gpx") 77 | log.Debug(string(body)) 78 | 79 | var r UploadTourResponse 80 | if err := json.Unmarshal(body, &r); err != nil { 81 | return nil, err 82 | } 83 | 84 | if len(r.Embedded.Items) != 1 { 85 | return nil, errors.New("import failed: " + r.Message) 86 | } 87 | 88 | return &r.Embedded.Items[0], nil 89 | } 90 | 91 | func (client *Client) importTour(tour *UploadedTour, sport string) (*MatchedTour, error) { 92 | tourJson, err := json.Marshal(tour) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | req, err := http.NewRequest( 98 | http.MethodPost, 99 | fmt.Sprintf("https://www%s/api/routing/import/tour?sport=%s&_embedded=way_types%%2Csurfaces%%2Cdirections%%2Ccoordinates", client.komootDomain, sport), 100 | bytes.NewBuffer(tourJson), 101 | ) 102 | if err != nil { 103 | return nil, err 104 | } 105 | req.Header.Set("Content-Type", contentTypeJson) 106 | req.Header.Set("Accept", acceptJson) 107 | req.Header.Add("Authorization", "Basic "+client.basicAuth()) 108 | 109 | resp, err := client.httpClient.Do(req) 110 | if err != nil { 111 | return nil, err 112 | } 113 | defer resp.Body.Close() 114 | 115 | body, err := io.ReadAll(resp.Body) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | log.DebugSeparator("import tour") 121 | log.DebugDump(string(body), "body:") 122 | 123 | var r UploadTourResponse 124 | if err := json.Unmarshal(body, &r); err != nil { 125 | return nil, err 126 | } 127 | 128 | return &r.Embedded.Matched, nil 129 | } 130 | 131 | func (client *Client) updateTourSettings(tour *MatchedTour, name string, sport string, makeRoundtrip bool, duration float64) *MatchedTour { 132 | tour.Name = name 133 | tour.Sport = sport 134 | tour.Status = "public" 135 | tour.Type = "tour_planned" 136 | tour.Constitution = 4 137 | tour.Duration = duration 138 | 139 | if makeRoundtrip { 140 | firstPoint := tour.Path[0] 141 | lastPoint := tour.Path[len(tour.Path)-1] 142 | firstPoint.Reference = "special:back" 143 | firstPoint.Index = lastPoint.Index + 1 144 | 145 | tour.Path = append(tour.Path, firstPoint) 146 | 147 | tour.Segments = append(tour.Segments, Segment{ 148 | From: lastPoint.Index, 149 | To: lastPoint.Index + 1, 150 | Type: "Routed", 151 | }) 152 | 153 | lastCoordinate := tour.Embedded.Coordinates.Items[len(tour.Embedded.Coordinates.Items)-1] 154 | 155 | tour.Embedded.Coordinates.Items = append(tour.Embedded.Coordinates.Items, Coordinate{ 156 | Lat: firstPoint.Location.Lat, 157 | Lng: firstPoint.Location.Lng, 158 | Alt: firstPoint.Location.Alt, 159 | T: lastCoordinate.T + 1, 160 | }) 161 | } 162 | 163 | return tour 164 | } 165 | 166 | func (client *Client) createTour(tour *MatchedTour) error { 167 | tourJson, err := json.Marshal(tour) 168 | if err != nil { 169 | return err 170 | } 171 | 172 | req, err := http.NewRequest( 173 | http.MethodPost, 174 | fmt.Sprintf("https://www%s/api/v007/tours/?reroute=true&hl=nl", client.komootDomain), 175 | bytes.NewBuffer(tourJson), 176 | ) 177 | if err != nil { 178 | return err 179 | } 180 | req.Header.Set("Content-Type", contentTypeJson) 181 | req.Header.Set("Accept", acceptJson) 182 | req.Header.Add("Authorization", "Basic "+client.basicAuth()) 183 | 184 | resp, err := client.httpClient.Do(req) 185 | if err != nil { 186 | return err 187 | } 188 | defer resp.Body.Close() 189 | 190 | body, err := io.ReadAll(resp.Body) 191 | if err != nil { 192 | return err 193 | } 194 | 195 | log.DebugSeparator("create tour") 196 | log.DebugDump(string(body), "body:") 197 | 198 | return nil 199 | } 200 | 201 | func (client *Client) planRoute(tour *MatchedTour) (float64, error) { 202 | paths := []RoutePlanRequestPath{} 203 | for _, path := range tour.Path { 204 | paths = append(paths, RoutePlanRequestPath{ 205 | Reference: path.Reference, 206 | Location: path.Location, 207 | }) 208 | } 209 | 210 | segments := []RoutePlanRequestSegment{} 211 | for _, segment := range tour.Segments { 212 | segments = append(segments, RoutePlanRequestSegment{ 213 | Type: segment.Type, 214 | }) 215 | } 216 | 217 | bodyJson, err := json.Marshal(RoutePlanRequest{ 218 | Constitution: 4, 219 | Path: paths, 220 | Segments: segments, 221 | Sport: tour.Sport, 222 | }) 223 | if err != nil { 224 | return 0, err 225 | } 226 | 227 | req, err := http.NewRequest( 228 | http.MethodPost, 229 | fmt.Sprintf(`https://www%s/api/routing/tour?sport=racebike&_embedded=coordinates%%2Cway_types%%2Csurfaces%%2Cdirections`, client.komootDomain), 230 | bytes.NewBuffer(bodyJson), 231 | ) 232 | if err != nil { 233 | return 0, err 234 | } 235 | req.Header.Set("Content-Type", contentTypeJson) 236 | req.Header.Set("Accept", acceptJson) 237 | req.Header.Add("Authorization", "Basic "+client.basicAuth()) 238 | 239 | resp, err := client.httpClient.Do(req) 240 | if err != nil { 241 | return 0, err 242 | } 243 | defer resp.Body.Close() 244 | 245 | body, err := io.ReadAll(resp.Body) 246 | if err != nil { 247 | return 0, err 248 | } 249 | 250 | log.DebugSeparator("plan tour") 251 | log.Debug(string(body)) 252 | 253 | var r RoutePlanResponse 254 | if err := json.Unmarshal(body, &r); err != nil { 255 | return 0, err 256 | } 257 | 258 | return r.Duration, nil 259 | } 260 | 261 | func (client *Client) deleteTour(tour Tour) error { 262 | req, err := http.NewRequest( 263 | http.MethodDelete, 264 | fmt.Sprintf("https://www%s/api/v007/tours/%d?hl=nl", client.komootDomain, tour.ID), 265 | nil, 266 | ) 267 | if err != nil { 268 | return err 269 | } 270 | req.Header.Set("Content-Type", contentTypeJson) 271 | req.Header.Set("Accept", acceptJson) 272 | req.Header.Add("Authorization", "Basic "+client.basicAuth()) 273 | 274 | resp, err := client.httpClient.Do(req) 275 | if err != nil { 276 | return err 277 | } 278 | defer resp.Body.Close() 279 | 280 | body, err := io.ReadAll(resp.Body) 281 | if err != nil { 282 | return err 283 | } 284 | 285 | log.DebugSeparator("delete tour") 286 | log.DebugDump(string(body), "body:") 287 | 288 | return nil 289 | 290 | } 291 | -------------------------------------------------------------------------------- /data/test.gpx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5/05 klein (41 km) 5 | 6 | 7 | 5/05 klein (41 km) 8 | 9 | Ride 10 | 11 | 12 | 7.010000000000001 13 | 14 | 15 | 7.8500000000000005 16 | 17 | 18 | 8.450000000000001 19 | 20 | 21 | 9.040000000000001 22 | 23 | 24 | 9.040000000000001 25 | 26 | 27 | 10.290000000000001 28 | 29 | 30 | 10.290000000000001 31 | 32 | 33 | 10.76 34 | 35 | 36 | 9.88 37 | 38 | 39 | 10.010000000000002 40 | 41 | 42 | 10.39 43 | 44 | 45 | 10.650000000000002 46 | 47 | 48 | 10.780000000000001 49 | 50 | 51 | 10.84 52 | 53 | 54 | 10.820000000000002 55 | 56 | 57 | 10.75 58 | 59 | 60 | 10.51 61 | 62 | 63 | 10.26 64 | 65 | 66 | 10.100000000000001 67 | 68 | 69 | 10.100000000000001 70 | 71 | 72 | 10.51 73 | 74 | 75 | 10.51 76 | 77 | 78 | 9.91 79 | 80 | 81 | 10.040000000000001 82 | 83 | 84 | 11.01 85 | 86 | 87 | 11.200000000000001 88 | 89 | 90 | 11.62 91 | 92 | 93 | 11.590000000000002 94 | 95 | 96 | 11.09 97 | 98 | 99 | 10.910000000000002 100 | 101 | 102 | 11.09 103 | 104 | 105 | 10.92 106 | 107 | 108 | 11.62 109 | 110 | 111 | 11.600000000000001 112 | 113 | 114 | 11.48 115 | 116 | 117 | 10.700000000000001 118 | 119 | 120 | 11.01 121 | 122 | 123 | 11.7 124 | 125 | 126 | 15.93 127 | 128 | 129 | 16.85 130 | 131 | 132 | 17.63 133 | 134 | 135 | 18.070000000000004 136 | 137 | 138 | 18.19 139 | 140 | 141 | 18.12 142 | 143 | 144 | 17.88 145 | 146 | 147 | 17.830000000000002 148 | 149 | 150 | 17.68 151 | 152 | 153 | 17.330000000000002 154 | 155 | 156 | 16.84 157 | 158 | 159 | 15.57 160 | 161 | 162 | 14.84 163 | 164 | 165 | 13.01 166 | 167 | 168 | 13.01 169 | 170 | 171 | 11.15 172 | 173 | 174 | 10.81 175 | 176 | 177 | 10.650000000000002 178 | 179 | 180 | 10.620000000000001 181 | 182 | 183 | 10.67 184 | 185 | 186 | 10.94 187 | 188 | 189 | 11.250000000000002 190 | 191 | 192 | 11.66 193 | 194 | 195 | 11.83 196 | 197 | 198 | 11.83 199 | 200 | 201 | 12.190000000000001 202 | 203 | 204 | 12.41 205 | 206 | 207 | 12.950000000000001 208 | 209 | 210 | 13.930000000000001 211 | 212 | 213 | 14.980000000000002 214 | 215 | 216 | 17.400000000000002 217 | 218 | 219 | 18.55 220 | 221 | 222 | 19.23 223 | 224 | 225 | 19.24 226 | 227 | 228 | 18.76 229 | 230 | 231 | 16.92 232 | 233 | 234 | 16.080000000000002 235 | 236 | 237 | 15.240000000000002 238 | 239 | 240 | 15.14 241 | 242 | 243 | 15.330000000000002 244 | 245 | 246 | 15.770000000000003 247 | 248 | 249 | 15.790000000000001 250 | 251 | 252 | 15.99 253 | 254 | 255 | 15.690000000000003 256 | 257 | 258 | 15.99 259 | 260 | 261 | 15.4 262 | 263 | 264 | 15.46 265 | 266 | 267 | 16.27 268 | 269 | 270 | 15.05 271 | 272 | 273 | 14.990000000000002 274 | 275 | 276 | 15.370000000000001 277 | 278 | 279 | 15.520000000000001 280 | 281 | 282 | 16.95 283 | 284 | 285 | 16.020000000000003 286 | 287 | 288 | 16.25 289 | 290 | 291 | 16.310000000000002 292 | 293 | 294 | 16.740000000000002 295 | 296 | 297 | 17.550000000000004 298 | 299 | 300 | 17.32 301 | 302 | 303 | 17.990000000000002 304 | 305 | 306 | 18.500000000000004 307 | 308 | 309 | 18.840000000000003 310 | 311 | 312 | 18.79 313 | 314 | 315 | 19.17 316 | 317 | 318 | 18.98 319 | 320 | 321 | 18.88 322 | 323 | 324 | 18.79 325 | 326 | 327 | 18.09 328 | 329 | 330 | 19.21 331 | 332 | 333 | 20.62 334 | 335 | 336 | 21.39 337 | 338 | 339 | 22.430000000000003 340 | 341 | 342 | 22.150000000000002 343 | 344 | 345 | 21.910000000000004 346 | 347 | 348 | 22.340000000000003 349 | 350 | 351 | 22.35 352 | 353 | 354 | 22.69 355 | 356 | 357 | 22.61 358 | 359 | 360 | 20.360000000000003 361 | 362 | 363 | 20.240000000000002 364 | 365 | 366 | 20.25 367 | 368 | 369 | 20.48 370 | 371 | 372 | 20.69 373 | 374 | 375 | 21.040000000000003 376 | 377 | 378 | 21.080000000000002 379 | 380 | 381 | 21.48 382 | 383 | 384 | 21.34 385 | 386 | 387 | 21.92 388 | 389 | 390 | 20.64 391 | 392 | 393 | 17.68 394 | 395 | 396 | 16.810000000000002 397 | 398 | 399 | 15.680000000000003 400 | 401 | 402 | 15.31 403 | 404 | 405 | 15.120000000000001 406 | 407 | 408 | 14.83 409 | 410 | 411 | 14.650000000000002 412 | 413 | 414 | 14.610000000000001 415 | 416 | 417 | 14.67 418 | 419 | 420 | 14.690000000000001 421 | 422 | 423 | 14.68 424 | 425 | 426 | 14.66 427 | 428 | 429 | 14.68 430 | 431 | 432 | 14.68 433 | 434 | 435 | 14.68 436 | 437 | 438 | 14.13 439 | 440 | 441 | 14.13 442 | 443 | 444 | 14.07 445 | 446 | 447 | 13.500000000000002 448 | 449 | 450 | 13.670000000000002 451 | 452 | 453 | 13.920000000000002 454 | 455 | 456 | 14.100000000000001 457 | 458 | 459 | 15.120000000000001 460 | 461 | 462 | 15.770000000000003 463 | 464 | 465 | 15.76 466 | 467 | 468 | 15.790000000000001 469 | 470 | 471 | 15.870000000000001 472 | 473 | 474 | 15.88 475 | 476 | 477 | 16.099999999999998 478 | 479 | 480 | 16.450000000000003 481 | 482 | 483 | 16.73 484 | 485 | 486 | 17.18 487 | 488 | 489 | 17.34 490 | 491 | 492 | 17.580000000000002 493 | 494 | 495 | 17.45 496 | 497 | 498 | 17.580000000000002 499 | 500 | 501 | 16.85 502 | 503 | 504 | 17.11 505 | 506 | 507 | 17.23 508 | 509 | 510 | 17.380000000000003 511 | 512 | 513 | 17.560000000000002 514 | 515 | 516 | 17.59 517 | 518 | 519 | 17.52 520 | 521 | 522 | 17.470000000000002 523 | 524 | 525 | 17.23 526 | 527 | 528 | 17.12 529 | 530 | 531 | 16.17 532 | 533 | 534 | 15.780000000000003 535 | 536 | 537 | 15.65 538 | 539 | 540 | 15.850000000000003 541 | 542 | 543 | 16.07 544 | 545 | 546 | 16.240000000000002 547 | 548 | 549 | 16.68 550 | 551 | 552 | 16.89 553 | 554 | 555 | 16.580000000000002 556 | 557 | 558 | 17.080000000000002 559 | 560 | 561 | 17.09 562 | 563 | 564 | 17.5 565 | 566 | 567 | 17.52 568 | 569 | 570 | 15.75 571 | 572 | 573 | 15.18 574 | 575 | 576 | 14.440000000000001 577 | 578 | 579 | 14.040000000000001 580 | 581 | 582 | 13.82 583 | 584 | 585 | 13.580000000000002 586 | 587 | 588 | 13.500000000000002 589 | 590 | 591 | 14.38 592 | 593 | 594 | 13.540000000000001 595 | 596 | 597 | 12.88 598 | 599 | 600 | 12.870000000000001 601 | 602 | 603 | 12.93 604 | 605 | 606 | 13.0 607 | 608 | 609 | 13.27 610 | 611 | 612 | 13.63 613 | 614 | 615 | 14.08 616 | 617 | 618 | 13.940000000000001 619 | 620 | 621 | 13.89 622 | 623 | 624 | 13.930000000000001 625 | 626 | 627 | 13.930000000000001 628 | 629 | 630 | 14.0 631 | 632 | 633 | 14.040000000000001 634 | 635 | 636 | 14.040000000000001 637 | 638 | 639 | 14.100000000000001 640 | 641 | 642 | 14.100000000000001 643 | 644 | 645 | 14.200000000000001 646 | 647 | 648 | 14.34 649 | 650 | 651 | 14.700000000000001 652 | 653 | 654 | 14.700000000000001 655 | 656 | 657 | 14.75 658 | 659 | 660 | 14.75 661 | 662 | 663 | 14.75 664 | 665 | 666 | 14.88 667 | 668 | 669 | 14.88 670 | 671 | 672 | 14.88 673 | 674 | 675 | 14.88 676 | 677 | 678 | 15.02 679 | 680 | 681 | 15.030000000000001 682 | 683 | 684 | 15.030000000000001 685 | 686 | 687 | 15.18 688 | 689 | 690 | 15.18 691 | 692 | 693 | 15.22 694 | 695 | 696 | 15.18 697 | 698 | 699 | 14.8 700 | 701 | 702 | 14.55 703 | 704 | 705 | 15.71 706 | 707 | 708 | 15.780000000000003 709 | 710 | 711 | 15.790000000000001 712 | 713 | 714 | 15.75 715 | 716 | 717 | 15.75 718 | 719 | 720 | 14.860000000000001 721 | 722 | 723 | 14.360000000000001 724 | 725 | 726 | 14.110000000000001 727 | 728 | 729 | 14.040000000000001 730 | 731 | 732 | 13.840000000000002 733 | 734 | 735 | 13.47 736 | 737 | 738 | 12.59 739 | 740 | 741 | 12.13 742 | 743 | 744 | 11.500000000000002 745 | 746 | 747 | 11.38 748 | 749 | 750 | 11.0 751 | 752 | 753 | 11.98 754 | 755 | 756 | 11.770000000000001 757 | 758 | 759 | 11.79 760 | 761 | 762 | 11.98 763 | 764 | 765 | 11.940000000000001 766 | 767 | 768 | 12.030000000000001 769 | 770 | 771 | 12.560000000000002 772 | 773 | 774 | 13.0 775 | 776 | 777 | 14.030000000000001 778 | 779 | 780 | 13.55 781 | 782 | 783 | 14.42 784 | 785 | 786 | 14.42 787 | 788 | 789 | 14.5 790 | 791 | 792 | 14.700000000000001 793 | 794 | 795 | 14.92 796 | 797 | 798 | 14.790000000000001 799 | 800 | 801 | 14.950000000000001 802 | 803 | 804 | 14.96 805 | 806 | 807 | 15.160000000000002 808 | 809 | 810 | 15.160000000000002 811 | 812 | 813 | 15.21 814 | 815 | 816 | 15.200000000000001 817 | 818 | 819 | 14.75 820 | 821 | 822 | 14.75 823 | 824 | 825 | 14.610000000000001 826 | 827 | 828 | 13.420000000000002 829 | 830 | 831 | 13.22 832 | 833 | 834 | 13.61 835 | 836 | 837 | 14.25 838 | 839 | 840 | 14.270000000000001 841 | 842 | 843 | 14.360000000000001 844 | 845 | 846 | 14.360000000000001 847 | 848 | 849 | 13.63 850 | 851 | 852 | 13.89 853 | 854 | 855 | 13.89 856 | 857 | 858 | 13.89 859 | 860 | 861 | 13.950000000000001 862 | 863 | 864 | 13.72 865 | 866 | 867 | 13.61 868 | 869 | 870 | 13.21 871 | 872 | 873 | 13.120000000000001 874 | 875 | 876 | 12.96 877 | 878 | 879 | 13.15 880 | 881 | 882 | 13.330000000000002 883 | 884 | 885 | 13.21 886 | 887 | 888 | 13.160000000000002 889 | 890 | 891 | 13.280000000000001 892 | 893 | 894 | 13.63 895 | 896 | 897 | 13.670000000000002 898 | 899 | 900 | 13.72 901 | 902 | 903 | 13.620000000000001 904 | 905 | 906 | 13.23 907 | 908 | 909 | 13.27 910 | 911 | 912 | 13.44 913 | 914 | 915 | 12.84 916 | 917 | 918 | 12.870000000000001 919 | 920 | 921 | 12.870000000000001 922 | 923 | 924 | 13.02 925 | 926 | 927 | 13.02 928 | 929 | 930 | 13.030000000000001 931 | 932 | 933 | 13.070000000000002 934 | 935 | 936 | 13.53 937 | 938 | 939 | 14.040000000000001 940 | 941 | 942 | 14.16 943 | 944 | 945 | 14.030000000000001 946 | 947 | 948 | 13.520000000000001 949 | 950 | 951 | 13.19 952 | 953 | 954 | 13.09 955 | 956 | 957 | 13.070000000000002 958 | 959 | 960 | 13.09 961 | 962 | 963 | 14.32 964 | 965 | 966 | 14.31 967 | 968 | 969 | 12.350000000000001 970 | 971 | 972 | 12.57 973 | 974 | 975 | 12.57 976 | 977 | 978 | 12.22 979 | 980 | 981 | 12.25 982 | 983 | 984 | 12.31 985 | 986 | 987 | 12.440000000000001 988 | 989 | 990 | 12.06 991 | 992 | 993 | 12.48 994 | 995 | 996 | 12.360000000000001 997 | 998 | 999 | 12.370000000000001 1000 | 1001 | 1002 | 12.180000000000001 1003 | 1004 | 1005 | 12.100000000000001 1006 | 1007 | 1008 | 12.93 1009 | 1010 | 1011 | 12.93 1012 | 1013 | 1014 | 13.250000000000002 1015 | 1016 | 1017 | 13.250000000000002 1018 | 1019 | 1020 | 13.44 1021 | 1022 | 1023 | 13.860000000000001 1024 | 1025 | 1026 | 13.96 1027 | 1028 | 1029 | 13.99 1030 | 1031 | 1032 | 13.9 1033 | 1034 | 1035 | 13.56 1036 | 1037 | 1038 | 13.56 1039 | 1040 | 1041 | 13.55 1042 | 1043 | 1044 | 13.670000000000002 1045 | 1046 | 1047 | 13.330000000000002 1048 | 1049 | 1050 | 13.22 1051 | 1052 | 1053 | 13.22 1054 | 1055 | 1056 | 13.27 1057 | 1058 | 1059 | 13.27 1060 | 1061 | 1062 | 13.27 1063 | 1064 | 1065 | 13.1 1066 | 1067 | 1068 | 13.1 1069 | 1070 | 1071 | 13.06 1072 | 1073 | 1074 | 13.3 1075 | 1076 | 1077 | 13.66 1078 | 1079 | 1080 | 14.100000000000001 1081 | 1082 | 1083 | 14.100000000000001 1084 | 1085 | 1086 | 14.020000000000001 1087 | 1088 | 1089 | 13.97 1090 | 1091 | 1092 | 13.83 1093 | 1094 | 1095 | 13.99 1096 | 1097 | 1098 | 14.040000000000001 1099 | 1100 | 1101 | 14.07 1102 | 1103 | 1104 | 14.110000000000001 1105 | 1106 | 1107 | 14.08 1108 | 1109 | 1110 | 14.08 1111 | 1112 | 1113 | 14.08 1114 | 1115 | 1116 | 14.0 1117 | 1118 | 1119 | 14.22 1120 | 1121 | 1122 | 14.15 1123 | 1124 | 1125 | 14.32 1126 | 1127 | 1128 | 14.42 1129 | 1130 | 1131 | 14.430000000000001 1132 | 1133 | 1134 | 14.55 1135 | 1136 | 1137 | 13.290000000000001 1138 | 1139 | 1140 | 12.89 1141 | 1142 | 1143 | 12.540000000000001 1144 | 1145 | 1146 | 12.34 1147 | 1148 | 1149 | 12.180000000000001 1150 | 1151 | 1152 | 12.0 1153 | 1154 | 1155 | 12.07 1156 | 1157 | 1158 | 11.89 1159 | 1160 | 1161 | 11.860000000000001 1162 | 1163 | 1164 | 11.860000000000001 1165 | 1166 | 1167 | 12.46 1168 | 1169 | 1170 | 14.25 1171 | 1172 | 1173 | 13.74 1174 | 1175 | 1176 | 12.64 1177 | 1178 | 1179 | 12.560000000000002 1180 | 1181 | 1182 | 12.360000000000001 1183 | 1184 | 1185 | 12.3 1186 | 1187 | 1188 | 12.040000000000001 1189 | 1190 | 1191 | 12.0 1192 | 1193 | 1194 | 11.959999999999999 1195 | 1196 | 1197 | 11.850000000000001 1198 | 1199 | 1200 | 11.81 1201 | 1202 | 1203 | 11.83 1204 | 1205 | 1206 | 11.9 1207 | 1208 | 1209 | 12.040000000000001 1210 | 1211 | 1212 | 12.06 1213 | 1214 | 1215 | 12.090000000000002 1216 | 1217 | 1218 | 12.090000000000002 1219 | 1220 | 1221 | 11.860000000000001 1222 | 1223 | 1224 | 11.860000000000001 1225 | 1226 | 1227 | 11.3 1228 | 1229 | 1230 | 11.61 1231 | 1232 | 1233 | 11.79 1234 | 1235 | 1236 | 11.79 1237 | 1238 | 1239 | 11.91 1240 | 1241 | 1242 | 12.08 1243 | 1244 | 1245 | 12.450000000000001 1246 | 1247 | 1248 | 12.560000000000002 1249 | 1250 | 1251 | 12.83 1252 | 1253 | 1254 | 12.83 1255 | 1256 | 1257 | 13.540000000000001 1258 | 1259 | 1260 | 13.83 1261 | 1262 | 1263 | 14.21 1264 | 1265 | 1266 | 14.21 1267 | 1268 | 1269 | 14.290000000000001 1270 | 1271 | 1272 | 14.63 1273 | 1274 | 1275 | 14.63 1276 | 1277 | 1278 | 14.63 1279 | 1280 | 1281 | 14.790000000000001 1282 | 1283 | 1284 | 15.06 1285 | 1286 | 1287 | 15.530000000000001 1288 | 1289 | 1290 | 15.850000000000003 1291 | 1292 | 1293 | 16.16 1294 | 1295 | 1296 | 16.400000000000002 1297 | 1298 | 1299 | 16.92 1300 | 1301 | 1302 | 17.380000000000003 1303 | 1304 | 1305 | 17.550000000000004 1306 | 1307 | 1308 | 17.640000000000004 1309 | 1310 | 1311 | 16.94 1312 | 1313 | 1314 | 17.75 1315 | 1316 | 1317 | 17.75 1318 | 1319 | 1320 | 17.84 1321 | 1322 | 1323 | 17.84 1324 | 1325 | 1326 | 17.84 1327 | 1328 | 1329 | 17.76 1330 | 1331 | 1332 | 17.76 1333 | 1334 | 1335 | 17.76 1336 | 1337 | 1338 | 17.16 1339 | 1340 | 1341 | 17.040000000000003 1342 | 1343 | 1344 | 17.27 1345 | 1346 | 1347 | 17.27 1348 | 1349 | 1350 | 17.66 1351 | 1352 | 1353 | 17.88 1354 | 1355 | 1356 | 17.890000000000004 1357 | 1358 | 1359 | 17.94 1360 | 1361 | 1362 | 17.93 1363 | 1364 | 1365 | 17.84 1366 | 1367 | 1368 | 17.720000000000002 1369 | 1370 | 1371 | 17.52 1372 | 1373 | 1374 | 17.52 1375 | 1376 | 1377 | 16.810000000000002 1378 | 1379 | 1380 | 16.61 1381 | 1382 | 1383 | 16.360000000000003 1384 | 1385 | 1386 | 15.8 1387 | 1388 | 1389 | 15.61 1390 | 1391 | 1392 | 15.320000000000002 1393 | 1394 | 1395 | 14.860000000000001 1396 | 1397 | 1398 | 14.790000000000001 1399 | 1400 | 1401 | 14.72 1402 | 1403 | 1404 | 14.67 1405 | 1406 | 1407 | 14.67 1408 | 1409 | 1410 | 14.650000000000002 1411 | 1412 | 1413 | 14.700000000000001 1414 | 1415 | 1416 | 14.700000000000001 1417 | 1418 | 1419 | 14.32 1420 | 1421 | 1422 | 14.72 1423 | 1424 | 1425 | 15.160000000000002 1426 | 1427 | 1428 | 15.870000000000001 1429 | 1430 | 1431 | 15.870000000000001 1432 | 1433 | 1434 | 16.09 1435 | 1436 | 1437 | 16.450000000000003 1438 | 1439 | 1440 | 16.360000000000003 1441 | 1442 | 1443 | 15.91 1444 | 1445 | 1446 | 15.84 1447 | 1448 | 1449 | 15.8 1450 | 1451 | 1452 | 15.8 1453 | 1454 | 1455 | 15.44 1456 | 1457 | 1458 | 15.44 1459 | 1460 | 1461 | 15.250000000000002 1462 | 1463 | 1464 | 15.02 1465 | 1466 | 1467 | 14.88 1468 | 1469 | 1470 | 14.010000000000002 1471 | 1472 | 1473 | 13.47 1474 | 1475 | 1476 | 13.250000000000002 1477 | 1478 | 1479 | 12.68 1480 | 1481 | 1482 | 12.68 1483 | 1484 | 1485 | 12.68 1486 | 1487 | 1488 | 11.930000000000001 1489 | 1490 | 1491 | 11.930000000000001 1492 | 1493 | 1494 | 11.74 1495 | 1496 | 1497 | 11.47 1498 | 1499 | 1500 | 11.62 1501 | 1502 | 1503 | 11.61 1504 | 1505 | 1506 | 11.430000000000001 1507 | 1508 | 1509 | 11.44 1510 | 1511 | 1512 | 11.21 1513 | 1514 | 1515 | 11.01 1516 | 1517 | 1518 | 10.81 1519 | 1520 | 1521 | 10.47 1522 | 1523 | 1524 | 10.15 1525 | 1526 | 1527 | 10.180000000000001 1528 | 1529 | 1530 | 9.65 1531 | 1532 | 1533 | 9.370000000000001 1534 | 1535 | 1536 | 10.06 1537 | 1538 | 1539 | 9.92 1540 | 1541 | 1542 | 9.97 1543 | 1544 | 1545 | 9.97 1546 | 1547 | 1548 | 9.97 1549 | 1550 | 1551 | 10.010000000000002 1552 | 1553 | 1554 | 10.030000000000001 1555 | 1556 | 1557 | 10.64 1558 | 1559 | 1560 | 10.98 1561 | 1562 | 1563 | 11.8 1564 | 1565 | 1566 | 12.430000000000001 1567 | 1568 | 1569 | 13.31 1570 | 1571 | 1572 | 13.840000000000002 1573 | 1574 | 1575 | 14.470000000000002 1576 | 1577 | 1578 | 14.25 1579 | 1580 | 1581 | 14.25 1582 | 1583 | 1584 | 14.780000000000001 1585 | 1586 | 1587 | 15.370000000000001 1588 | 1589 | 1590 | 16.53 1591 | 1592 | 1593 | 17.16 1594 | 1595 | 1596 | 17.7 1597 | 1598 | 1599 | 17.84 1600 | 1601 | 1602 | 17.830000000000002 1603 | 1604 | 1605 | 17.8 1606 | 1607 | 1608 | 17.890000000000004 1609 | 1610 | 1611 | 17.96 1612 | 1613 | 1614 | 18.21 1615 | 1616 | 1617 | 18.21 1618 | 1619 | 1620 | 18.21 1621 | 1622 | 1623 | 18.340000000000003 1624 | 1625 | 1626 | 18.3 1627 | 1628 | 1629 | 17.8 1630 | 1631 | 1632 | 17.580000000000002 1633 | 1634 | 1635 | 17.580000000000002 1636 | 1637 | 1638 | 16.71 1639 | 1640 | 1641 | 16.110000000000003 1642 | 1643 | 1644 | 16.110000000000003 1645 | 1646 | 1647 | 14.910000000000002 1648 | 1649 | 1650 | 14.57 1651 | 1652 | 1653 | 14.85 1654 | 1655 | 1656 | 14.610000000000001 1657 | 1658 | 1659 | 14.610000000000001 1660 | 1661 | 1662 | 14.560000000000002 1663 | 1664 | 1665 | 14.090000000000002 1666 | 1667 | 1668 | 14.100000000000001 1669 | 1670 | 1671 | 13.950000000000001 1672 | 1673 | 1674 | 14.0 1675 | 1676 | 1677 | 14.530000000000001 1678 | 1679 | 1680 | 14.470000000000002 1681 | 1682 | 1683 | 14.470000000000002 1684 | 1685 | 1686 | 14.5 1687 | 1688 | 1689 | 14.600000000000001 1690 | 1691 | 1692 | 14.600000000000001 1693 | 1694 | 1695 | 14.06 1696 | 1697 | 1698 | 14.14 1699 | 1700 | 1701 | 14.16 1702 | 1703 | 1704 | 13.950000000000001 1705 | 1706 | 1707 | 13.670000000000002 1708 | 1709 | 1710 | 13.31 1711 | 1712 | 1713 | 12.940000000000001 1714 | 1715 | 1716 | 12.24 1717 | 1718 | 1719 | 12.030000000000001 1720 | 1721 | 1722 | 11.98 1723 | 1724 | 1725 | 12.06 1726 | 1727 | 1728 | 12.15 1729 | 1730 | 1731 | 11.66 1732 | 1733 | 1734 | 11.070000000000002 1735 | 1736 | 1737 | 10.950000000000001 1738 | 1739 | 1740 | 10.64 1741 | 1742 | 1743 | 9.940000000000001 1744 | 1745 | 1746 | 9.1 1747 | 1748 | 1749 | 8.190000000000001 1750 | 1751 | 1752 | 8.3 1753 | 1754 | 1755 | 8.520000000000001 1756 | 1757 | 1758 | 8.17 1759 | 1760 | 1761 | 7.96 1762 | 1763 | 1764 | 7.8900000000000015 1765 | 1766 | 1767 | 7.640000000000001 1768 | 1769 | 1770 | 7.87 1771 | 1772 | 1773 | 8.51 1774 | 1775 | 1776 | 8.51 1777 | 1778 | 1779 | 8.5 1780 | 1781 | 1782 | 8.5 1783 | 1784 | 1785 | 8.040000000000001 1786 | 1787 | 1788 | 7.78 1789 | 1790 | 1791 | 7.51 1792 | 1793 | 1794 | 7.51 1795 | 1796 | 1797 | 7.0200000000000005 1798 | 1799 | 1800 | 6.99 1801 | 1802 | 1803 | 7.220000000000001 1804 | 1805 | 1806 | 7.51 1807 | 1808 | 1809 | 8.08 1810 | 1811 | 1812 | 8.25 1813 | 1814 | 1815 | 7.87 1816 | 1817 | 1818 | 7.69 1819 | 1820 | 1821 | 7.44 1822 | 1823 | 1824 | 7.51 1825 | 1826 | 1827 | 7.51 1828 | 1829 | 1830 | 7.51 1831 | 1832 | 1833 | 7.51 1834 | 1835 | 1836 | 7.430000000000001 1837 | 1838 | 1839 | 7.430000000000001 1840 | 1841 | 1842 | 7.5 1843 | 1844 | 1845 | 7.640000000000001 1846 | 1847 | 1848 | 7.640000000000001 1849 | 1850 | 1851 | 7.51 1852 | 1853 | 1854 | 7.29 1855 | 1856 | 1857 | 7.23 1858 | 1859 | 1860 | 7.450000000000001 1861 | 1862 | 1863 | 7.490000000000001 1864 | 1865 | 1866 | 7.390000000000001 1867 | 1868 | 1869 | 7.260000000000001 1870 | 1871 | 1872 | 7.57 1873 | 1874 | 1875 | 7.710000000000001 1876 | 1877 | 1878 | 7.95 1879 | 1880 | 1881 | 8.13 1882 | 1883 | 1884 | 8.56 1885 | 1886 | 1887 | 9.72 1888 | 1889 | 1890 | 11.53 1891 | 1892 | 1893 | 12.270000000000001 1894 | 1895 | 1896 | 12.270000000000001 1897 | 1898 | 1899 | 13.950000000000001 1900 | 1901 | 1902 | 15.19 1903 | 1904 | 1905 | 14.57 1906 | 1907 | 1908 | 12.25 1909 | 1910 | 1911 | 10.59 1912 | 1913 | 1914 | 9.590000000000002 1915 | 1916 | 1917 | 9.0 1918 | 1919 | 1920 | 8.76 1921 | 1922 | 1923 | 8.84 1924 | 1925 | 1926 | 8.84 1927 | 1928 | 1929 | 8.8 1930 | 1931 | 1932 | 8.56 1933 | 1934 | 1935 | 8.650000000000002 1936 | 1937 | 1938 | 8.81 1939 | 1940 | 1941 | 8.9 1942 | 1943 | 1944 | 8.55 1945 | 1946 | 1947 | 8.440000000000001 1948 | 1949 | 1950 | 7.95 1951 | 1952 | 1953 | 7.640000000000001 1954 | 1955 | 1956 | 7.37 1957 | 1958 | 1959 | 7.34 1960 | 1961 | 1962 | 7.1000000000000005 1963 | 1964 | 1965 | 6.66 1966 | 1967 | 1968 | 6.620000000000001 1969 | 1970 | 1971 | 6.65 1972 | 1973 | 1974 | 6.57 1975 | 1976 | 1977 | 6.470000000000001 1978 | 1979 | 1980 | 6.5200000000000005 1981 | 1982 | 1983 | 6.36 1984 | 1985 | 1986 | 6.220000000000001 1987 | 1988 | 1989 | 5.890000000000001 1990 | 1991 | 1992 | 5.0200000000000005 1993 | 1994 | 1995 | 4.44 1996 | 1997 | 1998 | 5.180000000000001 1999 | 2000 | 2001 | 5.17 2002 | 2003 | 2004 | 5.0 2005 | 2006 | 2007 | 5.750000000000001 2008 | 2009 | 2010 | 5.5600000000000005 2011 | 2012 | 2013 | 5.66 2014 | 2015 | 2016 | 5.800000000000001 2017 | 2018 | 2019 | 5.78 2020 | 2021 | 2022 | 5.94 2023 | 2024 | 2025 | 5.91 2026 | 2027 | 2028 | 6.010000000000001 2029 | 2030 | 2031 | 6.010000000000001 2032 | 2033 | 2034 | 5.9 2035 | 2036 | 2037 | 6.010000000000001 2038 | 2039 | 2040 | 6.280000000000001 2041 | 2042 | 2043 | 7.12 2044 | 2045 | 2046 | 7.53 2047 | 2048 | 2049 | 7.3100000000000005 2050 | 2051 | 2052 | 7.65 2053 | 2054 | 2055 | 8.0 2056 | 2057 | 2058 | 8.16 2059 | 2060 | 2061 | 7.8500000000000005 2062 | 2063 | 2064 | 7.87 2065 | 2066 | 2067 | 9.959999999999999 2068 | 2069 | 2070 | 11.0 2071 | 2072 | 2073 | 11.510000000000002 2074 | 2075 | 2076 | 11.770000000000001 2077 | 2078 | 2079 | 11.65 2080 | 2081 | 2082 | 11.430000000000001 2083 | 2084 | 2085 | 11.330000000000002 2086 | 2087 | 2088 | 10.48 2089 | 2090 | 2091 | 10.34 2092 | 2093 | 2094 | 10.22 2095 | 2096 | 2097 | 9.850000000000001 2098 | 2099 | 2100 | 9.73 2101 | 2102 | 2103 | 9.36 2104 | 2105 | 2106 | 8.26 2107 | 2108 | 2109 | 8.200000000000001 2110 | 2111 | 2112 | 8.26 2113 | 2114 | 2115 | 8.120000000000001 2116 | 2117 | 2118 | 8.190000000000001 2119 | 2120 | 2121 | 7.73 2122 | 2123 | 2124 | 7.74 2125 | 2126 | 2127 | 8.49 2128 | 2129 | 2130 | 9.250000000000002 2131 | 2132 | 2133 | 9.3 2134 | 2135 | 2136 | 9.61 2137 | 2138 | 2139 | 10.59 2140 | 2141 | 2142 | 10.66 2143 | 2144 | 2145 | 10.07 2146 | 2147 | 2148 | 10.66 2149 | 2150 | 2151 | 10.59 2152 | 2153 | 2154 | 10.42 2155 | 2156 | 2157 | 10.55 2158 | 2159 | 2160 | 10.55 2161 | 2162 | 2163 | 10.48 2164 | 2165 | 2166 | 10.41 2167 | 2168 | 2169 | 9.770000000000001 2170 | 2171 | 2172 | 10.100000000000001 2173 | 2174 | 2175 | 10.17 2176 | 2177 | 2178 | 10.17 2179 | 2180 | 2181 | 10.17 2182 | 2183 | 2184 | 9.98 2185 | 2186 | 2187 | 9.98 2188 | 2189 | 2190 | 10.06 2191 | 2192 | 2193 | 10.23 2194 | 2195 | 2196 | 10.790000000000001 2197 | 2198 | 2199 | 10.900000000000002 2200 | 2201 | 2202 | 11.340000000000002 2203 | 2204 | 2205 | 11.79 2206 | 2207 | 2208 | 11.23 2209 | 2210 | 2211 | 10.59 2212 | 2213 | 2214 | 10.520000000000001 2215 | 2216 | 2217 | 10.55 2218 | 2219 | 2220 | 10.41 2221 | 2222 | 2223 | 10.17 2224 | 2225 | 2226 | 10.290000000000001 2227 | 2228 | 2229 | 10.39 2230 | 2231 | 2232 | 10.3 2233 | 2234 | 2235 | 9.98 2236 | 2237 | 2238 | 10.440000000000001 2239 | 2240 | 2241 | 10.74 2242 | 2243 | 2244 | 10.5 2245 | 2246 | 2247 | 10.15 2248 | 2249 | 2250 | 11.1 2251 | 2252 | 2253 | 11.01 2254 | 2255 | 2256 | 11.0 2257 | 2258 | 2259 | 11.120000000000001 2260 | 2261 | 2262 | 11.3 2263 | 2264 | 2265 | 11.3 2266 | 2267 | 2268 | 11.370000000000001 2269 | 2270 | 2271 | 11.31 2272 | 2273 | 2274 | 11.11 2275 | 2276 | 2277 | 11.250000000000002 2278 | 2279 | 2280 | 11.22 2281 | 2282 | 2283 | 11.32 2284 | 2285 | 2286 | 11.200000000000001 2287 | 2288 | 2289 | 11.46 2290 | 2291 | 2292 | 11.39 2293 | 2294 | 2295 | 11.38 2296 | 2297 | 2298 | 11.120000000000001 2299 | 2300 | 2301 | 11.120000000000001 2302 | 2303 | 2304 | 10.49 2305 | 2306 | 2307 | 10.41 2308 | 2309 | 2310 | 10.560000000000002 2311 | 2312 | 2313 | 10.77 2314 | 2315 | 2316 | 10.74 2317 | 2318 | 2319 | 10.700000000000001 2320 | 2321 | 2322 | 10.93 2323 | 2324 | 2325 | 11.22 2326 | 2327 | 2328 | 11.290000000000001 2329 | 2330 | 2331 | 11.290000000000001 2332 | 2333 | 2334 | 11.290000000000001 2335 | 2336 | 2337 | 11.31 2338 | 2339 | 2340 | 11.160000000000002 2341 | 2342 | 2343 | 10.650000000000002 2344 | 2345 | 2346 | 10.650000000000002 2347 | 2348 | 2349 | 10.48 2350 | 2351 | 2352 | 9.850000000000001 2353 | 2354 | 2355 | 9.850000000000001 2356 | 2357 | 2358 | 7.540000000000001 2359 | 2360 | 2361 | 6.57 2362 | 2363 | 2364 | 6.74 2365 | 2366 | 2367 | 7.74 2368 | 2369 | 2370 | 8.010000000000002 2371 | 2372 | 2373 | 8.010000000000002 2374 | 2375 | 2376 | 7.280000000000001 2377 | 2378 | 2379 | 6.8100000000000005 2380 | 2381 | 2382 | 7.0600000000000005 2383 | 2384 | 2385 | 7.0600000000000005 2386 | 2387 | 2388 | 7.0 2389 | 2390 | 2391 | 2392 | 2393 | --------------------------------------------------------------------------------