merge trails function added (#627)
* 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>
This commit is contained in:
@@ -55,6 +55,10 @@ export default defineConfig({
|
||||
label: 'Summit logs',
|
||||
link: '/use/summit-logs/'
|
||||
},
|
||||
{
|
||||
label: 'Merge trails',
|
||||
link: '/use/merge-trails/'
|
||||
},
|
||||
{
|
||||
label: 'Interact with the community',
|
||||
link: '/use/community-interaction/'
|
||||
@@ -152,4 +156,4 @@ export default defineConfig({
|
||||
'/run/backup-server/': '/run/backend-configuration/backup-server/',
|
||||
'/run/custom-categories/': '/run/backend-configuration/custom-categories/',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
69
docs/src/content/docs/use/merge-trails.md
Normal file
69
docs/src/content/docs/use/merge-trails.md
Normal file
@@ -0,0 +1,69 @@
|
||||
---
|
||||
title: Merge trails
|
||||
description: Link repeated or duplicate trail recordings into a single target trail.
|
||||
---
|
||||
|
||||
<span class="-tracking-[0.075em]">wanderer</span> can link multiple trails into a single target trail. This is useful when the same route was recorded multiple times, imported from multiple services, or uploaded separately even though it belongs to the same underlying trail.
|
||||
|
||||
When a trail is merged, the source trail is converted into a summit log entry on the selected target trail. Depending on the chosen options, additional data such as photos, comments, tags and likes can also be merged.
|
||||
|
||||
## When to Use Trail Merging
|
||||
|
||||
Trail merging is helpful when:
|
||||
|
||||
- you recorded the same route multiple times and want one canonical trail page
|
||||
- a route was uploaded as a new trail, even though it was actually supposed to be a further iteration of an existing trail
|
||||
|
||||
If the route already exists and you simply want to log another outing, using a [summit log](/use/summit-logs) directly is usually the better choice.
|
||||
|
||||
## Manual Merge
|
||||
|
||||
You can merge trails manually from the trail actions menu:
|
||||
|
||||
- select multiple trails and choose **Link**
|
||||
- or open a single trail and choose **Merge with similar trail**
|
||||
|
||||
Before the merge is executed, <span class="-tracking-[0.075em]">wanderer</span> asks the backend for a suggested target trail. The backend uses the same target selection strategy in all merge modes and prefers trails that preserve the most useful information.
|
||||
|
||||
The target suggestion currently considers:
|
||||
|
||||
- existing summit logs
|
||||
- external references from integrations
|
||||
- content richness such as comments, photos, waypoints and descriptions
|
||||
- how centrally the trail geometry fits within the candidate set
|
||||
- trail age as a deterministic fallback
|
||||
|
||||
Warnings are shown before the merge if the selected trails differ noticeably in geometry or location.
|
||||
|
||||
## Automatic Matching for Similar Trails
|
||||
|
||||
The **Merge with similar trail** action searches for strong geometric matches of the currently open trail. Only trails with a sufficiently similar forward direction are considered. Out-and-back reversals are not treated as the same trail.
|
||||
|
||||
## Maintenance Page
|
||||
|
||||
The maintenance page groups potentially repeated or duplicate trails so that you can review them in batches:
|
||||
|
||||
- open **Settings → Repeated trails / duplicates**
|
||||
- inspect each group on the map
|
||||
- choose the target trail directly in the list
|
||||
- merge the group once you are satisfied
|
||||
|
||||
This page is especially useful after large imports or when you want to consolidate older data.
|
||||
|
||||
## Integrations
|
||||
|
||||
Integrations can optionally auto-merge imported trails, but only when the backend finds exactly one clear target candidate. This keeps imports conservative and avoids accidentally merging different routes.
|
||||
|
||||
External references from integrations are preserved during merges, so future imports can still recognize already-linked trails correctly.
|
||||
|
||||
## What Happens During a Merge
|
||||
|
||||
At a high level, the backend:
|
||||
|
||||
1. determines or receives a target trail
|
||||
2. creates a new summit log from the source trail on the target trail
|
||||
3. optionally merges existing summit logs, comments, likes, tags and photos
|
||||
4. reassigns external references to the target trail
|
||||
5. optionally deletes the source trail
|
||||
|
||||
The merge itself runs transactionally so partially completed merges are avoided.
|
||||
@@ -764,6 +764,125 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/trail-merge": {
|
||||
"post": {
|
||||
"summary": "Merge trails",
|
||||
"description": "Merges a source trail into a target trail using the backend merge logic.",
|
||||
"tags": [
|
||||
"Trail Merge"
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TrailMergeExecuteRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Merge acknowledged",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TrailMergeExecuteResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"403": {
|
||||
"description": "Forbidden",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/trail-merge/suggest": {
|
||||
"post": {
|
||||
"summary": "Suggest merge target or duplicate groups",
|
||||
"description": "Returns merge target suggestions for manual selection or auto-discovery, or temporary duplicate groups for maintenance workflows.",
|
||||
"tags": [
|
||||
"Trail Merge"
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TrailMergeSuggestRequest"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Suggestion response",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/TrailMergeSuggestResponse"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/TrailMergeSuggestGroupsResponse"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Unauthorized",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/Error"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/trail-link-share": {
|
||||
"get": {
|
||||
"summary": "List trail link shares",
|
||||
@@ -6571,6 +6690,201 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"TrailMergeSettings": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"summitLog",
|
||||
"photos",
|
||||
"comments",
|
||||
"delete",
|
||||
"tags",
|
||||
"likes"
|
||||
],
|
||||
"properties": {
|
||||
"summitLog": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"photos": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"comments": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"delete": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"tags": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"likes": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TrailMergeSuggestRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"mode"
|
||||
],
|
||||
"properties": {
|
||||
"mode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"manual-selection",
|
||||
"auto-discovery",
|
||||
"maintenance-groups"
|
||||
]
|
||||
},
|
||||
"trailIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Required for manual-selection"
|
||||
},
|
||||
"sourceTrailId": {
|
||||
"type": "string",
|
||||
"description": "Required for auto-discovery"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TrailMergeSuggestCandidate": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"trailId",
|
||||
"score",
|
||||
"reason",
|
||||
"warnings",
|
||||
"selectable"
|
||||
],
|
||||
"properties": {
|
||||
"trailId": {
|
||||
"type": "string"
|
||||
},
|
||||
"score": {
|
||||
"type": "number"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"selectable": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TrailMergeSuggestResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"targetTrailId",
|
||||
"reason",
|
||||
"warnings",
|
||||
"candidates"
|
||||
],
|
||||
"properties": {
|
||||
"targetTrailId": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
},
|
||||
"warnings": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"candidates": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TrailMergeSuggestCandidate"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"TrailMergeSuggestGroup": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"groupId",
|
||||
"trailIds",
|
||||
"targetTrailId",
|
||||
"reason",
|
||||
"score",
|
||||
"indirect"
|
||||
],
|
||||
"properties": {
|
||||
"groupId": {
|
||||
"type": "string"
|
||||
},
|
||||
"trailIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"targetTrailId": {
|
||||
"type": "string"
|
||||
},
|
||||
"reason": {
|
||||
"type": "string"
|
||||
},
|
||||
"score": {
|
||||
"type": "number"
|
||||
},
|
||||
"indirect": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TrailMergeSuggestGroupsResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"groups"
|
||||
],
|
||||
"properties": {
|
||||
"groups": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TrailMergeSuggestGroup"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"TrailMergeExecuteRequest": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"sourceTrailId",
|
||||
"targetTrailId",
|
||||
"settings"
|
||||
],
|
||||
"properties": {
|
||||
"sourceTrailId": {
|
||||
"type": "string"
|
||||
},
|
||||
"targetTrailId": {
|
||||
"type": "string"
|
||||
},
|
||||
"settings": {
|
||||
"$ref": "#/components/schemas/TrailMergeSettings"
|
||||
}
|
||||
}
|
||||
},
|
||||
"TrailMergeExecuteResponse": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"acknowledged"
|
||||
],
|
||||
"properties": {
|
||||
"acknowledged": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ListResult": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user