Trail Edit / Waypoints from photos: prevent creating different waypoints for near-by locations (#457)
* trail edit / waypoints from photos: prevent creating different waypoints for near-by locations * remove not necessary imports * remove changes from gitignore * patch gitignore * waypoint merge radius as category property * change datamodel for merge radius (use more generic settings field for future implementations) * fix npm run check issues * several improvements/fixes * fix * move geo cluster code to db * fix compile issue * fix docker issue * merge with existing waypoints --------- Co-authored-by: Christian Beutel <> Co-authored-by: Flomp <Flomp@users.noreply.github.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -11,4 +11,4 @@ run.sh
|
|||||||
build*.sh
|
build*.sh
|
||||||
start*.*
|
start*.*
|
||||||
|
|
||||||
data*/
|
data*/
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
# Unreleased
|
||||||
|
## Features
|
||||||
|
- Geotagged waypoint photos are now grouped into one waypoint when they are within the category's waypoint merge radius. Existing categories are initialized with waypoint merging enabled and a 50m merge radius; set `settings.wp_merge_enabled` to `false` on a category to keep creating one waypoint per photo.
|
||||||
|
|
||||||
# v0.18.5
|
# v0.18.5
|
||||||
## Security
|
## Security
|
||||||
- Fixes CVE-2022-39299 via xmldom upgrade (PR #820)
|
- Fixes CVE-2022-39299 via xmldom upgrade (PR #820)
|
||||||
@@ -682,4 +686,4 @@ As the number of contributors to this project continues to grow (which I’m ver
|
|||||||
- updated the docs to include BODY_SIZE_LIMIT
|
- updated the docs to include BODY_SIZE_LIMIT
|
||||||
|
|
||||||
# v0.1.0
|
# v0.1.0
|
||||||
- Initial release
|
- Initial release
|
||||||
|
|||||||
@@ -6,4 +6,6 @@
|
|||||||
!main.go
|
!main.go
|
||||||
!migrations
|
!migrations
|
||||||
!templates
|
!templates
|
||||||
|
!waypointcluster
|
||||||
|
!waypointcluster/**
|
||||||
!util
|
!util
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import (
|
|||||||
|
|
||||||
_ "pocketbase/migrations"
|
_ "pocketbase/migrations"
|
||||||
"pocketbase/util"
|
"pocketbase/util"
|
||||||
|
"pocketbase/waypointcluster"
|
||||||
|
|
||||||
pub "github.com/go-ap/activitypub"
|
pub "github.com/go-ap/activitypub"
|
||||||
"github.com/microcosm-cc/bluemonday"
|
"github.com/microcosm-cc/bluemonday"
|
||||||
@@ -1072,6 +1073,8 @@ func registerRoutes(se *core.ServeEvent, client meilisearch.ServiceManager) {
|
|||||||
return e.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
return e.JSON(http.StatusOK, map[string]string{"status": "ok"})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
se.Router.POST("/waypoint/cluster", waypointcluster.Handler)
|
||||||
|
|
||||||
se.Router.POST("/auth/token", func(e *core.RequestEvent) error {
|
se.Router.POST("/auth/token", func(e *core.RequestEvent) error {
|
||||||
var data struct {
|
var data struct {
|
||||||
APIToken string `json:"api_token"`
|
APIToken string `json:"api_token"`
|
||||||
@@ -1488,6 +1491,10 @@ func bootstrapCategories(app core.App) error {
|
|||||||
for _, element := range categories {
|
for _, element := range categories {
|
||||||
record := core.NewRecord(collection)
|
record := core.NewRecord(collection)
|
||||||
record.Set("name", element)
|
record.Set("name", element)
|
||||||
|
record.Set("settings", map[string]any{
|
||||||
|
"wp_merge_enabled": true,
|
||||||
|
"wp_merge_radius": 50,
|
||||||
|
})
|
||||||
f, _ := filesystem.NewFileFromPath("migrations/initial_data/" + strings.ToLower(element) + ".jpg")
|
f, _ := filesystem.NewFileFromPath("migrations/initial_data/" + strings.ToLower(element) + ".jpg")
|
||||||
record.Set("img", f)
|
record.Set("img", f)
|
||||||
err := app.Save(record)
|
err := app.Save(record)
|
||||||
|
|||||||
60
db/migrations/1763300311_updated_categories.go
Normal file
60
db/migrations/1763300311_updated_categories.go
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
package migrations
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
m "github.com/pocketbase/pocketbase/migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
m.Register(func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("kjxvi8asj2igqwf")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// add field
|
||||||
|
if err := collection.Fields.AddMarshaledJSONAt(6, []byte(`{
|
||||||
|
"hidden": false,
|
||||||
|
"id": "json3846545605",
|
||||||
|
"maxSize": 0,
|
||||||
|
"name": "settings",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "json"
|
||||||
|
}`)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := app.Save(collection); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
records, err := app.FindAllRecords("categories")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, record := range records {
|
||||||
|
record.Set("settings", map[string]any{
|
||||||
|
"wp_merge_enabled": true,
|
||||||
|
"wp_merge_radius": 50,
|
||||||
|
})
|
||||||
|
if err := app.Save(record); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, func(app core.App) error {
|
||||||
|
collection, err := app.FindCollectionByNameOrId("kjxvi8asj2igqwf")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove field
|
||||||
|
collection.Fields.RemoveById("json3846545605")
|
||||||
|
|
||||||
|
return app.Save(collection)
|
||||||
|
})
|
||||||
|
}
|
||||||
14
db/util/geo.go
Normal file
14
db/util/geo.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
func HaversineDistance(lat1 float64, lon1 float64, lat2 float64, lon2 float64) float64 {
|
||||||
|
const earthRadiusKm = 6371
|
||||||
|
dLat := (lat2 - lat1) * (math.Pi / 180)
|
||||||
|
dLon := (lon2 - lon1) * (math.Pi / 180)
|
||||||
|
a := math.Sin(dLat/2)*math.Sin(dLat/2) +
|
||||||
|
math.Cos(lat1*(math.Pi/180))*math.Cos(lat2*(math.Pi/180))*
|
||||||
|
math.Sin(dLon/2)*math.Sin(dLon/2)
|
||||||
|
c := 2 * math.Atan2(math.Sqrt(a), math.Sqrt(1-a))
|
||||||
|
return earthRadiusKm * c * 1000
|
||||||
|
}
|
||||||
197
db/waypointcluster/waypoint_cluster.go
Normal file
197
db/waypointcluster/waypoint_cluster.go
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
package waypointcluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/pocketbase/pocketbase/apis"
|
||||||
|
"github.com/pocketbase/pocketbase/core"
|
||||||
|
|
||||||
|
"pocketbase/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultWaypointMergeRadius = 50
|
||||||
|
|
||||||
|
type waypointMergeSettings struct {
|
||||||
|
Enabled bool
|
||||||
|
Radius float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type waypointClusterRequest struct {
|
||||||
|
Category string `json:"category"`
|
||||||
|
Photos []waypointClusterPhoto `json:"photos"`
|
||||||
|
Waypoints []waypointClusterWaypoint `json:"waypoints"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type waypointClusterPhoto struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Lat float64 `json:"lat"`
|
||||||
|
Lon float64 `json:"lon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type waypointClusterWaypoint struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Lat float64 `json:"lat"`
|
||||||
|
Lon float64 `json:"lon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type waypointPhotoCluster struct {
|
||||||
|
Waypoint string `json:"waypoint,omitempty"`
|
||||||
|
Photos []string `json:"photos"`
|
||||||
|
SumLat float64 `json:"-"`
|
||||||
|
SumLon float64 `json:"-"`
|
||||||
|
Count int `json:"-"`
|
||||||
|
Lat float64 `json:"lat"`
|
||||||
|
Lon float64 `json:"lon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type categorySettings struct {
|
||||||
|
WaypointMergeEnabled *bool `json:"wp_merge_enabled"`
|
||||||
|
WaypointMergeRadius *float64 `json:"wp_merge_radius"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Handler(e *core.RequestEvent) error {
|
||||||
|
if e.Auth == nil {
|
||||||
|
return apis.NewUnauthorizedError("authentication required", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
var data waypointClusterRequest
|
||||||
|
if err := e.BindBody(&data); err != nil {
|
||||||
|
return apis.NewBadRequestError("Failed to read request data", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if data.Category != "" && len(data.Category) != 15 {
|
||||||
|
return apis.NewBadRequestError("Invalid category", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, photo := range data.Photos {
|
||||||
|
if photo.ID == "" {
|
||||||
|
return apis.NewBadRequestError("Invalid photo id", nil)
|
||||||
|
}
|
||||||
|
if photo.Lat < -90 || photo.Lat > 90 {
|
||||||
|
return apis.NewBadRequestError("Invalid photo latitude", nil)
|
||||||
|
}
|
||||||
|
if photo.Lon < -180 || photo.Lon > 180 {
|
||||||
|
return apis.NewBadRequestError("Invalid photo longitude", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, waypoint := range data.Waypoints {
|
||||||
|
if waypoint.ID == "" {
|
||||||
|
return apis.NewBadRequestError("Invalid waypoint id", nil)
|
||||||
|
}
|
||||||
|
if waypoint.Lat < -90 || waypoint.Lat > 90 {
|
||||||
|
return apis.NewBadRequestError("Invalid waypoint latitude", nil)
|
||||||
|
}
|
||||||
|
if waypoint.Lon < -180 || waypoint.Lon > 180 {
|
||||||
|
return apis.NewBadRequestError("Invalid waypoint longitude", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mergeSettings, err := getWaypointMergeSettings(e.App, data.Category)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return e.JSON(http.StatusOK, map[string]any{
|
||||||
|
"mergeEnabled": mergeSettings.Enabled,
|
||||||
|
"mergeRadius": mergeSettings.Radius,
|
||||||
|
"clusters": clusterWaypointPhotos(data.Photos, data.Waypoints, mergeSettings),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWaypointMergeSettings(app core.App, categoryId string) (waypointMergeSettings, error) {
|
||||||
|
defaultSettings := waypointMergeSettings{
|
||||||
|
Enabled: true,
|
||||||
|
Radius: defaultWaypointMergeRadius,
|
||||||
|
}
|
||||||
|
|
||||||
|
if categoryId == "" {
|
||||||
|
return defaultSettings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
category, err := app.FindRecordById("categories", categoryId)
|
||||||
|
if err != nil {
|
||||||
|
return waypointMergeSettings{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings categorySettings
|
||||||
|
if err := category.UnmarshalJSONField("settings", &settings); err != nil {
|
||||||
|
return defaultSettings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.WaypointMergeEnabled != nil {
|
||||||
|
defaultSettings.Enabled = *settings.WaypointMergeEnabled
|
||||||
|
}
|
||||||
|
|
||||||
|
if settings.WaypointMergeRadius != nil && *settings.WaypointMergeRadius >= 0 {
|
||||||
|
defaultSettings.Radius = *settings.WaypointMergeRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultSettings, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func clusterWaypointPhotos(photos []waypointClusterPhoto, waypoints []waypointClusterWaypoint, mergeSettings waypointMergeSettings) []waypointPhotoCluster {
|
||||||
|
clusters := []waypointPhotoCluster{}
|
||||||
|
|
||||||
|
if mergeSettings.Enabled {
|
||||||
|
for _, waypoint := range waypoints {
|
||||||
|
clusters = append(clusters, newWaypointCluster(waypoint))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, photo := range photos {
|
||||||
|
if !mergeSettings.Enabled {
|
||||||
|
clusters = append(clusters, newWaypointPhotoCluster(photo))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
matchingClusterIndex := -1
|
||||||
|
for i, cluster := range clusters {
|
||||||
|
distanceToCenter := util.HaversineDistance(cluster.Lat, cluster.Lon, photo.Lat, photo.Lon)
|
||||||
|
if distanceToCenter <= mergeSettings.Radius {
|
||||||
|
matchingClusterIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if matchingClusterIndex >= 0 {
|
||||||
|
addPhotoToWaypointCluster(&clusters[matchingClusterIndex], photo)
|
||||||
|
} else {
|
||||||
|
clusters = append(clusters, newWaypointPhotoCluster(photo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return clusters
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWaypointPhotoCluster(photo waypointClusterPhoto) waypointPhotoCluster {
|
||||||
|
return waypointPhotoCluster{
|
||||||
|
Photos: []string{photo.ID},
|
||||||
|
SumLat: photo.Lat,
|
||||||
|
SumLon: photo.Lon,
|
||||||
|
Count: 1,
|
||||||
|
Lat: photo.Lat,
|
||||||
|
Lon: photo.Lon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newWaypointCluster(waypoint waypointClusterWaypoint) waypointPhotoCluster {
|
||||||
|
return waypointPhotoCluster{
|
||||||
|
Waypoint: waypoint.ID,
|
||||||
|
Photos: []string{},
|
||||||
|
SumLat: waypoint.Lat,
|
||||||
|
SumLon: waypoint.Lon,
|
||||||
|
Count: 1,
|
||||||
|
Lat: waypoint.Lat,
|
||||||
|
Lon: waypoint.Lon,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func addPhotoToWaypointCluster(cluster *waypointPhotoCluster, photo waypointClusterPhoto) {
|
||||||
|
cluster.Photos = append(cluster.Photos, photo.ID)
|
||||||
|
cluster.SumLat += photo.Lat
|
||||||
|
cluster.SumLon += photo.Lon
|
||||||
|
cluster.Count++
|
||||||
|
cluster.Lat = cluster.SumLat / float64(cluster.Count)
|
||||||
|
cluster.Lon = cluster.SumLon / float64(cluster.Count)
|
||||||
|
}
|
||||||
@@ -2,6 +2,10 @@
|
|||||||
title: Changelog
|
title: Changelog
|
||||||
description: What changed in the last patch?
|
description: What changed in the last patch?
|
||||||
---
|
---
|
||||||
|
## Unreleased
|
||||||
|
### Features
|
||||||
|
- Geotagged waypoint photos are now grouped into one waypoint when they are within the category's waypoint merge radius. Existing categories are initialized with waypoint merging enabled and a 50m merge radius; set `settings.wp_merge_enabled` to `false` on a category to keep creating one waypoint per photo.
|
||||||
|
|
||||||
## v0.18.5
|
## v0.18.5
|
||||||
### Security
|
### Security
|
||||||
- Fixes CVE-2022-39299 via xmldom upgrade
|
- Fixes CVE-2022-39299 via xmldom upgrade
|
||||||
@@ -682,4 +686,4 @@ This version updates the index pattern of the meilisearch index. Please delete o
|
|||||||
- updated the docs to include BODY_SIZE_LIMIT
|
- updated the docs to include BODY_SIZE_LIMIT
|
||||||
|
|
||||||
## v0.1.0
|
## v0.1.0
|
||||||
- Initial release
|
- Initial release
|
||||||
|
|||||||
@@ -16,3 +16,24 @@ All existing categories will be listed here.
|
|||||||
To edit one simply click on the row, edit the data you want to change, and click "Save".
|
To edit one simply click on the row, edit the data you want to change, and click "Save".
|
||||||
To delete a category check the box at the beginning of the row and click "Delete selected".
|
To delete a category check the box at the beginning of the row and click "Delete selected".
|
||||||
To create a new category click the "New record" button in the top right corner, give your new category a name and a background image, and click "Save".
|
To create a new category click the "New record" button in the top right corner, give your new category a name and a background image, and click "Save".
|
||||||
|
|
||||||
|
## Category settings
|
||||||
|
|
||||||
|
Categories can optionally define additional settings in the `settings` JSON field.
|
||||||
|
This field may be left empty.
|
||||||
|
When no settings are configured, <span class="-tracking-[0.075em]">wanderer</span> uses the built-in defaults.
|
||||||
|
|
||||||
|
Currently, the following setting is supported:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"wp_merge_enabled": true,
|
||||||
|
"wp_merge_radius": 50
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`wp_merge_enabled` controls whether geotagged photos are grouped into waypoint clusters.
|
||||||
|
Set it to `false` to create one waypoint per photo.
|
||||||
|
|
||||||
|
`wp_merge_radius` controls how close geotagged photos have to be to each other, in meters, before they are grouped into the same waypoint when adding waypoint photos to a trail.
|
||||||
|
Set it to `0` to only merge photos with the exact same coordinates, or increase the value to merge photos across a wider area.
|
||||||
|
|||||||
248
web/src/lib/components/waypoint/waypoint_merge_modal.svelte
Normal file
248
web/src/lib/components/waypoint/waypoint_merge_modal.svelte
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
<script module lang="ts">
|
||||||
|
import type { Waypoint } from "$lib/models/waypoint";
|
||||||
|
|
||||||
|
export type WaypointMerge = {
|
||||||
|
incoming: Waypoint;
|
||||||
|
existing: Waypoint;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type WaypointMergeOptions = {
|
||||||
|
photos: boolean;
|
||||||
|
title: boolean;
|
||||||
|
description: boolean;
|
||||||
|
icon: boolean;
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { browser } from "$app/environment";
|
||||||
|
import { _ } from "svelte-i18n";
|
||||||
|
import Modal from "../base/modal.svelte";
|
||||||
|
import RadioGroup, { type RadioItem } from "../base/radio_group.svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
merge?: WaypointMerge;
|
||||||
|
oncreate?: () => void;
|
||||||
|
onmerge?: (options: WaypointMergeOptions) => void;
|
||||||
|
oncancel?: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { merge, oncreate, onmerge, oncancel }: Props = $props();
|
||||||
|
|
||||||
|
const waypointMergeActionItems: RadioItem[] = [
|
||||||
|
{
|
||||||
|
text: $_("create-waypoint-anyway"),
|
||||||
|
value: "create",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
text: $_("add-to-existing-waypoint"),
|
||||||
|
value: "merge",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let modal: Modal;
|
||||||
|
let waypointMergeAction: "merge" | "create" = $state("create");
|
||||||
|
let waypointMergeActionIndex = $derived(
|
||||||
|
waypointMergeActionItems.findIndex(
|
||||||
|
(item) => item.value === waypointMergeAction,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
let appendWaypointPhotos = $state(true);
|
||||||
|
let appendWaypointTitle = $state(true);
|
||||||
|
let appendWaypointDescription = $state(true);
|
||||||
|
let appendWaypointIcon = $state(false);
|
||||||
|
|
||||||
|
export function openModal() {
|
||||||
|
loadWaypointMergePreferences();
|
||||||
|
modal.openModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeModal() {
|
||||||
|
modal.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveDecision() {
|
||||||
|
if (waypointMergeAction === "create") {
|
||||||
|
oncreate?.();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onmerge?.({
|
||||||
|
photos: appendWaypointPhotos,
|
||||||
|
title: appendWaypointTitle,
|
||||||
|
description: appendWaypointDescription,
|
||||||
|
icon: appendWaypointIcon,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadWaypointMergePreferences() {
|
||||||
|
waypointMergeAction = getWaypointMergePreference("action", false)
|
||||||
|
? "merge"
|
||||||
|
: "create";
|
||||||
|
appendWaypointPhotos = getWaypointMergePreference("photos", true);
|
||||||
|
appendWaypointTitle = getWaypointMergePreference("title", true);
|
||||||
|
appendWaypointDescription = getWaypointMergePreference(
|
||||||
|
"description",
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
appendWaypointIcon = getWaypointMergePreference("icon", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWaypointMergePreference(key: string, fallback: boolean) {
|
||||||
|
if (!browser) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = localStorage.getItem(`waypoint-merge-${key}`);
|
||||||
|
if (value == null) {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value === "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
function setWaypointMergePreference(key: string, value: boolean) {
|
||||||
|
if (browser) {
|
||||||
|
localStorage.setItem(`waypoint-merge-${key}`, value.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Modal
|
||||||
|
id="waypoint-merge-modal"
|
||||||
|
title={$_("nearby-waypoint-found")}
|
||||||
|
bind:this={modal}
|
||||||
|
>
|
||||||
|
{#snippet content()}
|
||||||
|
{#if merge}
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-3 rounded-md border border-input-border p-3"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-surface"
|
||||||
|
>
|
||||||
|
<i class="fa fa-{merge.existing.icon ?? 'circle'}"></i>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p class="font-medium">
|
||||||
|
{merge.existing.name ||
|
||||||
|
$_("waypoints", { values: { n: 1 } })}
|
||||||
|
</p>
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
{merge.existing.lat.toFixed(5)},
|
||||||
|
{merge.existing.lon.toFixed(5)}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
{$_("nearby-waypoint-found-text", {
|
||||||
|
values: {
|
||||||
|
name:
|
||||||
|
merge.existing.name ||
|
||||||
|
$_("waypoints", { values: { n: 1 } }),
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
<RadioGroup
|
||||||
|
name="waypoint-merge-action"
|
||||||
|
items={waypointMergeActionItems}
|
||||||
|
selected={waypointMergeActionIndex}
|
||||||
|
onchange={(item) => {
|
||||||
|
waypointMergeAction = item.value as "merge" | "create";
|
||||||
|
setWaypointMergePreference(
|
||||||
|
"action",
|
||||||
|
waypointMergeAction === "merge",
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
></RadioGroup>
|
||||||
|
{#if waypointMergeAction === "merge"}
|
||||||
|
<div class="space-y-2 pl-6">
|
||||||
|
{#if merge.incoming._photos?.length}
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={appendWaypointPhotos}
|
||||||
|
onchange={() =>
|
||||||
|
setWaypointMergePreference(
|
||||||
|
"photos",
|
||||||
|
appendWaypointPhotos,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span>{$_("append-waypoint-photos")}</span>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
{#if merge.incoming.name?.trim()}
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={appendWaypointTitle}
|
||||||
|
onchange={() =>
|
||||||
|
setWaypointMergePreference(
|
||||||
|
"title",
|
||||||
|
appendWaypointTitle,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{merge.existing.name?.trim()
|
||||||
|
? $_("append-waypoint-title")
|
||||||
|
: $_("use-waypoint-title")}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
{#if merge.incoming.description?.trim()}
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={appendWaypointDescription}
|
||||||
|
onchange={() =>
|
||||||
|
setWaypointMergePreference(
|
||||||
|
"description",
|
||||||
|
appendWaypointDescription,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{merge.existing.description?.trim()
|
||||||
|
? $_("append-waypoint-description")
|
||||||
|
: $_("use-waypoint-description")}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
{#if merge.incoming.icon &&
|
||||||
|
merge.incoming.icon !== merge.existing.icon}
|
||||||
|
<label class="flex items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
bind:checked={appendWaypointIcon}
|
||||||
|
onchange={() =>
|
||||||
|
setWaypointMergePreference(
|
||||||
|
"icon",
|
||||||
|
appendWaypointIcon,
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
{$_("use-new-waypoint-icon")}
|
||||||
|
<i
|
||||||
|
class="fa fa-{merge.incoming.icon} ml-1"
|
||||||
|
></i>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
{#snippet footer()}
|
||||||
|
<div class="flex flex-wrap items-center gap-4">
|
||||||
|
<button class="btn-secondary" type="button" onclick={oncancel}
|
||||||
|
>{$_("cancel")}</button
|
||||||
|
>
|
||||||
|
<button class="btn-primary" type="button" onclick={saveDecision}
|
||||||
|
>{waypointMergeAction === "create"
|
||||||
|
? $_("save")
|
||||||
|
: $_("continue")}</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
</Modal>
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: Snippet<[any]>;
|
children?: Snippet<[any]>;
|
||||||
onsave?: (waypoint: Waypoint) => void
|
onsave?: (waypoint: Waypoint) => boolean | Promise<boolean> | void
|
||||||
}
|
}
|
||||||
|
|
||||||
let { children, onsave }: Props = $props();
|
let { children, onsave }: Props = $props();
|
||||||
@@ -32,6 +32,10 @@
|
|||||||
modal.openModal();
|
modal.openModal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function closeModal() {
|
||||||
|
modal.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
const ClientWaypointCreateSchema = WaypointCreateSchema.extend({
|
const ClientWaypointCreateSchema = WaypointCreateSchema.extend({
|
||||||
_photos: z.array(z.instanceof(File)).optional(),
|
_photos: z.array(z.instanceof(File)).optional(),
|
||||||
});
|
});
|
||||||
@@ -42,9 +46,11 @@
|
|||||||
initialValues: $waypoint,
|
initialValues: $waypoint,
|
||||||
extend: validator({ schema: ClientWaypointCreateSchema }),
|
extend: validator({ schema: ClientWaypointCreateSchema }),
|
||||||
onSubmit: async (form) => {
|
onSubmit: async (form) => {
|
||||||
onsave?.(form);
|
const shouldClose = await onsave?.(form);
|
||||||
|
|
||||||
modal.closeModal!();
|
if (shouldClose !== false) {
|
||||||
|
modal.closeModal!();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
transform: (values: unknown) => {
|
transform: (values: unknown) => {
|
||||||
const v = values as any;
|
const v = values as any;
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
"no-data": "Žádná data",
|
"no-data": "Žádná data",
|
||||||
"no-description-for-now": "Zatím bez popisu",
|
"no-description-for-now": "Zatím bez popisu",
|
||||||
"no-gps-data-in-image": "Obrázek neobsahuje GPS data",
|
"no-gps-data-in-image": "Obrázek neobsahuje GPS data",
|
||||||
|
"waypoint-cluster-error": "Could not create waypoint clusters",
|
||||||
"no-grid": "Bez mřížky",
|
"no-grid": "Bez mřížky",
|
||||||
"no-notifications": "Žádná upozornění",
|
"no-notifications": "Žádná upozornění",
|
||||||
"no-photos-here": "Zde nejsou žádné fotky ani videa",
|
"no-photos-here": "Zde nejsou žádné fotky ani videa",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"activity": "{n, plural, =1 {Aktivität} other {Aktivitäten}}",
|
"activity": "{n, plural, =1 {Aktivität} other {Aktivitäten}}",
|
||||||
"add-bio": "Bio hinzufügen",
|
"add-bio": "Bio hinzufügen",
|
||||||
"add-entry": "Eintrag hinzufügen",
|
"add-entry": "Eintrag hinzufügen",
|
||||||
|
"add-to-existing-waypoint": "Zu bestehendem Wegpunkt hinzufügen",
|
||||||
"add-to-list": "Listen verwalten",
|
"add-to-list": "Listen verwalten",
|
||||||
"add-waypoint": "Wegpunkt hinzufügen",
|
"add-waypoint": "Wegpunkt hinzufügen",
|
||||||
"added-trail-to": "Route hinzugefügt zu",
|
"added-trail-to": "Route hinzugefügt zu",
|
||||||
@@ -26,6 +27,9 @@
|
|||||||
"api-documentation": "API Dokumentation",
|
"api-documentation": "API Dokumentation",
|
||||||
"api-tokens": "",
|
"api-tokens": "",
|
||||||
"api-tokens-hint": "",
|
"api-tokens-hint": "",
|
||||||
|
"append-waypoint-description": "Kommentar anhängen",
|
||||||
|
"append-waypoint-photos": "Fotos hinzufügen",
|
||||||
|
"append-waypoint-title": "Titel anhängen",
|
||||||
"apply-user-settings": "",
|
"apply-user-settings": "",
|
||||||
"attraction": "Sehenswürdigkeit",
|
"attraction": "Sehenswürdigkeit",
|
||||||
"author": "Autor",
|
"author": "Autor",
|
||||||
@@ -84,10 +88,12 @@
|
|||||||
"confirm-publish": "Veröffentlichung bestätigen",
|
"confirm-publish": "Veröffentlichung bestätigen",
|
||||||
"confirm-share": "Teilen bestätigen",
|
"confirm-share": "Teilen bestätigen",
|
||||||
"connect": "Verbinden",
|
"connect": "Verbinden",
|
||||||
|
"continue": "Fortfahren",
|
||||||
"contribute": "Mitwirken",
|
"contribute": "Mitwirken",
|
||||||
"copy-link": "Link kopieren",
|
"copy-link": "Link kopieren",
|
||||||
"create-new-list": "Neue Liste erstellen",
|
"create-new-list": "Neue Liste erstellen",
|
||||||
"create-waypoint": "Wegpunkt erstellen",
|
"create-waypoint": "Wegpunkt erstellen",
|
||||||
|
"create-waypoint-anyway": "Trotzdem erstellen",
|
||||||
"creation-date": "Erstellungsdatum",
|
"creation-date": "Erstellungsdatum",
|
||||||
"crop": "Zuschneiden",
|
"crop": "Zuschneiden",
|
||||||
"cross": "Querfeldein",
|
"cross": "Querfeldein",
|
||||||
@@ -286,6 +292,8 @@
|
|||||||
"n-years-ago": "vor {n} Jahren",
|
"n-years-ago": "vor {n} Jahren",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"near": "Nahe",
|
"near": "Nahe",
|
||||||
|
"nearby-waypoint-found": "Wegpunkt in der Nähe gefunden",
|
||||||
|
"nearby-waypoint-found-text": "„{name}“ liegt nah genug, um mit diesem Wegpunkt zusammengeführt zu werden. Was möchtest du tun?",
|
||||||
"never": "",
|
"never": "",
|
||||||
"new-list": "Neue Liste",
|
"new-list": "Neue Liste",
|
||||||
"new-password": "Neues Passwort",
|
"new-password": "Neues Passwort",
|
||||||
@@ -301,6 +309,7 @@
|
|||||||
"no-data": "Keine Daten",
|
"no-data": "Keine Daten",
|
||||||
"no-description-for-now": "Noch keine Beschreibung",
|
"no-description-for-now": "Noch keine Beschreibung",
|
||||||
"no-gps-data-in-image": "Keine GPS-Daten im Bild",
|
"no-gps-data-in-image": "Keine GPS-Daten im Bild",
|
||||||
|
"waypoint-cluster-error": "Wegpunkt-Gruppen konnten nicht erstellt werden",
|
||||||
"no-grid": "Kein Gitter",
|
"no-grid": "Kein Gitter",
|
||||||
"no-notifications": "Keine Benachrichtigungen",
|
"no-notifications": "Keine Benachrichtigungen",
|
||||||
"no-photos-here": "Hier sind noch keine Fotos",
|
"no-photos-here": "Hier sind noch keine Fotos",
|
||||||
@@ -457,6 +466,9 @@
|
|||||||
"upload-new-file": "Neue Datei hochladen",
|
"upload-new-file": "Neue Datei hochladen",
|
||||||
"uploaded": "hochgeladen",
|
"uploaded": "hochgeladen",
|
||||||
"uploaded-trail-to-hammerhead": "Route erfolgreich zu Hammerhead hochgeladen",
|
"uploaded-trail-to-hammerhead": "Route erfolgreich zu Hammerhead hochgeladen",
|
||||||
|
"use-new-waypoint-icon": "Icon ersetzen",
|
||||||
|
"use-waypoint-description": "Kommentar übernehmen",
|
||||||
|
"use-waypoint-title": "Titel übernehmen",
|
||||||
"use-hills": "Hügel einbeziehen",
|
"use-hills": "Hügel einbeziehen",
|
||||||
"use-roads": "Nutze Straßen",
|
"use-roads": "Nutze Straßen",
|
||||||
"username": "Nutzername",
|
"username": "Nutzername",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"activity": "{n, plural, =1 {Activity} other {Activities}}",
|
"activity": "{n, plural, =1 {Activity} other {Activities}}",
|
||||||
"add-bio": "Add Bio",
|
"add-bio": "Add Bio",
|
||||||
"add-entry": "Add Entry",
|
"add-entry": "Add Entry",
|
||||||
|
"add-to-existing-waypoint": "Add to existing waypoint",
|
||||||
"add-to-list": "Manage lists",
|
"add-to-list": "Manage lists",
|
||||||
"add-waypoint": "Add Waypoint",
|
"add-waypoint": "Add Waypoint",
|
||||||
"added-trail-to": "Added trail to",
|
"added-trail-to": "Added trail to",
|
||||||
@@ -26,6 +27,9 @@
|
|||||||
"api-documentation": "API Documentation",
|
"api-documentation": "API Documentation",
|
||||||
"api-tokens": "API Tokens",
|
"api-tokens": "API Tokens",
|
||||||
"api-tokens-hint": "API Tokens can be used to grant 3rd party applications access to your wanderer account.",
|
"api-tokens-hint": "API Tokens can be used to grant 3rd party applications access to your wanderer account.",
|
||||||
|
"append-waypoint-description": "Append description",
|
||||||
|
"append-waypoint-photos": "Add photos",
|
||||||
|
"append-waypoint-title": "Append title",
|
||||||
"apply-user-settings": "Apply user settings",
|
"apply-user-settings": "Apply user settings",
|
||||||
"attraction": "Attraction",
|
"attraction": "Attraction",
|
||||||
"author": "Author",
|
"author": "Author",
|
||||||
@@ -84,10 +88,12 @@
|
|||||||
"confirm-publish": "Confirm publishing",
|
"confirm-publish": "Confirm publishing",
|
||||||
"confirm-share": "Confirm share",
|
"confirm-share": "Confirm share",
|
||||||
"connect": "Connect",
|
"connect": "Connect",
|
||||||
|
"continue": "Continue",
|
||||||
"contribute": "Contribute",
|
"contribute": "Contribute",
|
||||||
"copy-link": "Copy Link",
|
"copy-link": "Copy Link",
|
||||||
"create-new-list": "Create new list",
|
"create-new-list": "Create new list",
|
||||||
"create-waypoint": "Create waypoint",
|
"create-waypoint": "Create waypoint",
|
||||||
|
"create-waypoint-anyway": "Create anyway",
|
||||||
"creation-date": "Creation date",
|
"creation-date": "Creation date",
|
||||||
"crop": "Crop",
|
"crop": "Crop",
|
||||||
"cross": "Cross",
|
"cross": "Cross",
|
||||||
@@ -286,6 +292,8 @@
|
|||||||
"n-years-ago": "{n} years ago",
|
"n-years-ago": "{n} years ago",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"near": "Near",
|
"near": "Near",
|
||||||
|
"nearby-waypoint-found": "Nearby waypoint found",
|
||||||
|
"nearby-waypoint-found-text": "\"{name}\" is close enough to merge with this waypoint. What would you like to do?",
|
||||||
"never": "Never",
|
"never": "Never",
|
||||||
"new-list": "New List",
|
"new-list": "New List",
|
||||||
"new-password": "New password",
|
"new-password": "New password",
|
||||||
@@ -301,6 +309,7 @@
|
|||||||
"no-data": "No data",
|
"no-data": "No data",
|
||||||
"no-description-for-now": "No description for now",
|
"no-description-for-now": "No description for now",
|
||||||
"no-gps-data-in-image": "No GPS data in image",
|
"no-gps-data-in-image": "No GPS data in image",
|
||||||
|
"waypoint-cluster-error": "Could not create waypoint clusters",
|
||||||
"no-grid": "No Grid",
|
"no-grid": "No Grid",
|
||||||
"no-notifications": "No notifications",
|
"no-notifications": "No notifications",
|
||||||
"no-photos-here": "No photos or videos here",
|
"no-photos-here": "No photos or videos here",
|
||||||
@@ -457,6 +466,9 @@
|
|||||||
"upload-new-file": "Upload new file",
|
"upload-new-file": "Upload new file",
|
||||||
"uploaded": "uploaded",
|
"uploaded": "uploaded",
|
||||||
"uploaded-trail-to-hammerhead": "Successfully uploaded trail to Hammerhead",
|
"uploaded-trail-to-hammerhead": "Successfully uploaded trail to Hammerhead",
|
||||||
|
"use-new-waypoint-icon": "Replace icon",
|
||||||
|
"use-waypoint-description": "Use description",
|
||||||
|
"use-waypoint-title": "Use title",
|
||||||
"use-hills": "Use hills",
|
"use-hills": "Use hills",
|
||||||
"use-roads": "Use Roads",
|
"use-roads": "Use Roads",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
"no-data": "No datos",
|
"no-data": "No datos",
|
||||||
"no-description-for-now": "Ninguna descripción de momento",
|
"no-description-for-now": "Ninguna descripción de momento",
|
||||||
"no-gps-data-in-image": "Sin datos GPS en la imagen",
|
"no-gps-data-in-image": "Sin datos GPS en la imagen",
|
||||||
|
"waypoint-cluster-error": "Could not create waypoint clusters",
|
||||||
"no-grid": "Ninguna cuadrícula",
|
"no-grid": "Ninguna cuadrícula",
|
||||||
"no-notifications": "No notificaciones",
|
"no-notifications": "No notificaciones",
|
||||||
"no-photos-here": "No fotos aquí",
|
"no-photos-here": "No fotos aquí",
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
"no-data": "Ez dago daturik",
|
"no-data": "Ez dago daturik",
|
||||||
"no-description-for-now": "Ez dago deskribapenik",
|
"no-description-for-now": "Ez dago deskribapenik",
|
||||||
"no-gps-data-in-image": "Ez dago GPS daturik irudian",
|
"no-gps-data-in-image": "Ez dago GPS daturik irudian",
|
||||||
|
"waypoint-cluster-error": "Could not create waypoint clusters",
|
||||||
"no-grid": "Ez dago saretarik",
|
"no-grid": "Ez dago saretarik",
|
||||||
"no-notifications": "Ez dago jakinarazpenik",
|
"no-notifications": "Ez dago jakinarazpenik",
|
||||||
"no-photos-here": "Ez dago argazki edo bideorik",
|
"no-photos-here": "Ez dago argazki edo bideorik",
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
"no-data": "Pas de données",
|
"no-data": "Pas de données",
|
||||||
"no-description-for-now": "Pas de description pour le moment",
|
"no-description-for-now": "Pas de description pour le moment",
|
||||||
"no-gps-data-in-image": "Aucune donnée GPS dans l'image",
|
"no-gps-data-in-image": "Aucune donnée GPS dans l'image",
|
||||||
|
"waypoint-cluster-error": "Could not create waypoint clusters",
|
||||||
"no-grid": "Aucune grille",
|
"no-grid": "Aucune grille",
|
||||||
"no-notifications": "Pas de notifications",
|
"no-notifications": "Pas de notifications",
|
||||||
"no-photos-here": "Aucune photo ici",
|
"no-photos-here": "Aucune photo ici",
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
"no-data": "No data",
|
"no-data": "No data",
|
||||||
"no-description-for-now": "No description for now",
|
"no-description-for-now": "No description for now",
|
||||||
"no-gps-data-in-image": "No GPS data in image",
|
"no-gps-data-in-image": "No GPS data in image",
|
||||||
|
"waypoint-cluster-error": "Could not create waypoint clusters",
|
||||||
"no-grid": "No Grid",
|
"no-grid": "No Grid",
|
||||||
"no-notifications": "No notifications",
|
"no-notifications": "No notifications",
|
||||||
"no-photos-here": "No photos here",
|
"no-photos-here": "No photos here",
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
"no-data": "Nessun dato",
|
"no-data": "Nessun dato",
|
||||||
"no-description-for-now": "Nessuna descrizione per il momento",
|
"no-description-for-now": "Nessuna descrizione per il momento",
|
||||||
"no-gps-data-in-image": "No GPS data in image",
|
"no-gps-data-in-image": "No GPS data in image",
|
||||||
|
"waypoint-cluster-error": "Could not create waypoint clusters",
|
||||||
"no-grid": "Nessuna griglia",
|
"no-grid": "Nessuna griglia",
|
||||||
"no-notifications": "Nessuna notifica",
|
"no-notifications": "Nessuna notifica",
|
||||||
"no-photos-here": "Nessuna foto qui",
|
"no-photos-here": "Nessuna foto qui",
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
"no-data": "Geen data",
|
"no-data": "Geen data",
|
||||||
"no-description-for-now": "Voorlopig geen beschrijving",
|
"no-description-for-now": "Voorlopig geen beschrijving",
|
||||||
"no-gps-data-in-image": "Geen GPS data in afbeelding",
|
"no-gps-data-in-image": "Geen GPS data in afbeelding",
|
||||||
|
"waypoint-cluster-error": "Could not create waypoint clusters",
|
||||||
"no-grid": "Geen raster",
|
"no-grid": "Geen raster",
|
||||||
"no-notifications": "Geen meldingen",
|
"no-notifications": "Geen meldingen",
|
||||||
"no-photos-here": "No photos here",
|
"no-photos-here": "No photos here",
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
"no-data": "Ingen data",
|
"no-data": "Ingen data",
|
||||||
"no-description-for-now": "Ingen beskrivelse ennå",
|
"no-description-for-now": "Ingen beskrivelse ennå",
|
||||||
"no-gps-data-in-image": "Ingen GPS-data i bildet",
|
"no-gps-data-in-image": "Ingen GPS-data i bildet",
|
||||||
|
"waypoint-cluster-error": "Could not create waypoint clusters",
|
||||||
"no-grid": "Ingen rutenett",
|
"no-grid": "Ingen rutenett",
|
||||||
"no-notifications": "Ingen varsler",
|
"no-notifications": "Ingen varsler",
|
||||||
"no-photos-here": "Ingen bilder eller videoer her",
|
"no-photos-here": "Ingen bilder eller videoer her",
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
"no-data": "Brak danych",
|
"no-data": "Brak danych",
|
||||||
"no-description-for-now": "Nie ma jeszcze opisu",
|
"no-description-for-now": "Nie ma jeszcze opisu",
|
||||||
"no-gps-data-in-image": "No GPS data in image",
|
"no-gps-data-in-image": "No GPS data in image",
|
||||||
|
"waypoint-cluster-error": "Could not create waypoint clusters",
|
||||||
"no-grid": "Brak Siatki",
|
"no-grid": "Brak Siatki",
|
||||||
"no-notifications": "Brak powiadomień",
|
"no-notifications": "Brak powiadomień",
|
||||||
"no-photos-here": "Nie ma tu zdjęć",
|
"no-photos-here": "Nie ma tu zdjęć",
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
"no-data": "Sem dados",
|
"no-data": "Sem dados",
|
||||||
"no-description-for-now": "No description for now",
|
"no-description-for-now": "No description for now",
|
||||||
"no-gps-data-in-image": "No GPS data in image",
|
"no-gps-data-in-image": "No GPS data in image",
|
||||||
|
"waypoint-cluster-error": "Could not create waypoint clusters",
|
||||||
"no-grid": "No Grid",
|
"no-grid": "No Grid",
|
||||||
"no-notifications": "No notifications",
|
"no-notifications": "No notifications",
|
||||||
"no-photos-here": "No photos here",
|
"no-photos-here": "No photos here",
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
"no-data": "Нет данных",
|
"no-data": "Нет данных",
|
||||||
"no-description-for-now": "Пока нет описания",
|
"no-description-for-now": "Пока нет описания",
|
||||||
"no-gps-data-in-image": "No GPS data in image",
|
"no-gps-data-in-image": "No GPS data in image",
|
||||||
|
"waypoint-cluster-error": "Could not create waypoint clusters",
|
||||||
"no-grid": "Без сетки",
|
"no-grid": "Без сетки",
|
||||||
"no-notifications": "Нет уведомлений",
|
"no-notifications": "Нет уведомлений",
|
||||||
"no-photos-here": "Здесь нет фото/видео",
|
"no-photos-here": "Здесь нет фото/видео",
|
||||||
|
|||||||
@@ -301,6 +301,7 @@
|
|||||||
"no-data": "无数据",
|
"no-data": "无数据",
|
||||||
"no-description-for-now": "暂无描述",
|
"no-description-for-now": "暂无描述",
|
||||||
"no-gps-data-in-image": "图像中没有GPS数据",
|
"no-gps-data-in-image": "图像中没有GPS数据",
|
||||||
|
"waypoint-cluster-error": "Could not create waypoint clusters",
|
||||||
"no-grid": "无网格",
|
"no-grid": "无网格",
|
||||||
"no-notifications": "没有通知",
|
"no-notifications": "没有通知",
|
||||||
"no-photos-here": "No photos here",
|
"no-photos-here": "No photos here",
|
||||||
|
|||||||
@@ -2,6 +2,13 @@ interface Category {
|
|||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
img: string;
|
img: string;
|
||||||
|
settings?: Settings | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type {Category}
|
interface Settings {
|
||||||
|
wp_merge_enabled?: boolean;
|
||||||
|
wp_merge_radius?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type {Category}
|
||||||
|
export type {Settings}
|
||||||
|
|||||||
@@ -221,17 +221,37 @@ export async function trails_create(trail: Trail, photos: File[], gpx: File | Bl
|
|||||||
|
|
||||||
let model: Trail = await r.json();
|
let model: Trail = await r.json();
|
||||||
|
|
||||||
|
const createdSummitLogs: SummitLog[] = [];
|
||||||
for (const summitLog of trail.expand?.summit_logs_via_trail ?? []) {
|
for (const summitLog of trail.expand?.summit_logs_via_trail ?? []) {
|
||||||
summitLog.trail = model.id!;
|
summitLog.trail = model.id!;
|
||||||
await summit_logs_create(summitLog, f);
|
createdSummitLogs.push(await summit_logs_create(summitLog, f));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createdWaypoints: Waypoint[] = [];
|
||||||
for (const wp of trail.expand?.waypoints_via_trail ?? []) {
|
for (const wp of trail.expand?.waypoints_via_trail ?? []) {
|
||||||
wp.trail = model.id!;
|
wp.trail = model.id!;
|
||||||
await waypoints_create({
|
createdWaypoints.push(await waypoints_create({
|
||||||
...wp,
|
...wp,
|
||||||
marker: undefined,
|
marker: undefined,
|
||||||
}, f, user);
|
}, f, user));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!model.expand) {
|
||||||
|
model.expand = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createdSummitLogs.length) {
|
||||||
|
model.expand.summit_logs_via_trail = [
|
||||||
|
...(model.expand.summit_logs_via_trail ?? []),
|
||||||
|
...createdSummitLogs,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (createdWaypoints.length) {
|
||||||
|
model.expand.waypoints_via_trail = [
|
||||||
|
...(model.expand.waypoints_via_trail ?? []),
|
||||||
|
...createdWaypoints,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
|
|||||||
@@ -10,6 +10,9 @@
|
|||||||
import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
|
import MapWithElevationMaplibre from "$lib/components/trail/map_with_elevation_maplibre.svelte";
|
||||||
import PhotoPicker from "$lib/components/trail/photo_picker.svelte";
|
import PhotoPicker from "$lib/components/trail/photo_picker.svelte";
|
||||||
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
import WaypointCard from "$lib/components/waypoint/waypoint_card.svelte";
|
||||||
|
import WaypointMergeModal, {
|
||||||
|
type WaypointMergeOptions,
|
||||||
|
} from "$lib/components/waypoint/waypoint_merge_modal.svelte";
|
||||||
import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte";
|
import WaypointModal from "$lib/components/waypoint/waypoint_modal.svelte";
|
||||||
import { SummitLogCreateSchema } from "$lib/models/api/summit_log_schema.js";
|
import { SummitLogCreateSchema } from "$lib/models/api/summit_log_schema.js";
|
||||||
import { TrailCreateSchema } from "$lib/models/api/trail_schema.js";
|
import { TrailCreateSchema } from "$lib/models/api/trail_schema.js";
|
||||||
@@ -74,6 +77,7 @@
|
|||||||
import RouteEditor from "$lib/components/trail/route_editor.svelte";
|
import RouteEditor from "$lib/components/trail/route_editor.svelte";
|
||||||
import { TagCreateSchema } from "$lib/models/api/tag_schema.js";
|
import { TagCreateSchema } from "$lib/models/api/tag_schema.js";
|
||||||
import { convertDMSToDD } from "$lib/models/gpx/utils.js";
|
import { convertDMSToDD } from "$lib/models/gpx/utils.js";
|
||||||
|
import { getPb } from "$lib/pocketbase";
|
||||||
import { Tag } from "$lib/models/tag.js";
|
import { Tag } from "$lib/models/tag.js";
|
||||||
import {
|
import {
|
||||||
searchLocationReverse,
|
searchLocationReverse,
|
||||||
@@ -93,10 +97,10 @@
|
|||||||
import cryptoRandomString from "crypto-random-string";
|
import cryptoRandomString from "crypto-random-string";
|
||||||
import { createForm } from "felte";
|
import { createForm } from "felte";
|
||||||
import * as M from "maplibre-gl";
|
import * as M from "maplibre-gl";
|
||||||
import { onMount, tick, untrack } from "svelte";
|
import { onMount, untrack } from "svelte";
|
||||||
import { _ } from "svelte-i18n";
|
import { _ } from "svelte-i18n";
|
||||||
import { backInOut } from "svelte/easing";
|
import { backInOut } from "svelte/easing";
|
||||||
import { fly, slide } from "svelte/transition";
|
import { fly } from "svelte/transition";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import Track from "$lib/models/gpx/track.js";
|
import Track from "$lib/models/gpx/track.js";
|
||||||
import TrackSegment from "$lib/models/gpx/track-segment.js";
|
import TrackSegment from "$lib/models/gpx/track-segment.js";
|
||||||
@@ -110,6 +114,7 @@
|
|||||||
let lists = $state(untrack(() => data.lists));
|
let lists = $state(untrack(() => data.lists));
|
||||||
|
|
||||||
let waypointModal: WaypointModal;
|
let waypointModal: WaypointModal;
|
||||||
|
let waypointMergeModal: WaypointMergeModal;
|
||||||
let summitLogModal: SummitLogModal;
|
let summitLogModal: SummitLogModal;
|
||||||
let listSelectModal: ListSearchModal;
|
let listSelectModal: ListSearchModal;
|
||||||
let markTrailAsCompletedModal: ConfirmModal;
|
let markTrailAsCompletedModal: ConfirmModal;
|
||||||
@@ -132,6 +137,10 @@
|
|||||||
}
|
}
|
||||||
let overwriteGPX = false;
|
let overwriteGPX = false;
|
||||||
let draggingMarker = false;
|
let draggingMarker = false;
|
||||||
|
|
||||||
|
let pendingWaypointMerge:
|
||||||
|
| { incoming: Waypoint; existing: Waypoint }
|
||||||
|
| undefined = $state();
|
||||||
|
|
||||||
let searchDropdownItems: SearchItem[] = $state([]);
|
let searchDropdownItems: SearchItem[] = $state([]);
|
||||||
|
|
||||||
@@ -466,7 +475,7 @@
|
|||||||
// updateTrailOnMap();
|
// updateTrailOnMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveWaypoint(savedWaypoint: Waypoint) {
|
function commitWaypoint(savedWaypoint: Waypoint) {
|
||||||
let editedWaypointIndex =
|
let editedWaypointIndex =
|
||||||
$formData.expand!.waypoints_via_trail?.findIndex(
|
$formData.expand!.waypoints_via_trail?.findIndex(
|
||||||
(s) => s.id == savedWaypoint.id,
|
(s) => s.id == savedWaypoint.id,
|
||||||
@@ -486,6 +495,171 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getExistingWaypointClusterInputs() {
|
||||||
|
return (
|
||||||
|
$formData.expand?.waypoints_via_trail
|
||||||
|
?.filter((wp) => wp.id)
|
||||||
|
.map((wp) => ({
|
||||||
|
id: wp.id!,
|
||||||
|
lat: wp.lat,
|
||||||
|
lon: wp.lon,
|
||||||
|
})) ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveWaypoint(savedWaypoint: Waypoint) {
|
||||||
|
const editedWaypointIndex =
|
||||||
|
$formData.expand!.waypoints_via_trail?.findIndex(
|
||||||
|
(s) => s.id == savedWaypoint.id,
|
||||||
|
) ?? -1;
|
||||||
|
|
||||||
|
if (editedWaypointIndex >= 0) {
|
||||||
|
commitWaypoint(savedWaypoint);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingWaypoint = await findMergeableWaypoint(savedWaypoint);
|
||||||
|
if (matchingWaypoint) {
|
||||||
|
pendingWaypointMerge = {
|
||||||
|
incoming: savedWaypoint,
|
||||||
|
existing: matchingWaypoint,
|
||||||
|
};
|
||||||
|
waypointModal.closeModal();
|
||||||
|
waypointMergeModal.openModal();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
commitWaypoint(savedWaypoint);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findMergeableWaypoint(savedWaypoint: Waypoint) {
|
||||||
|
const existingWaypoints = getExistingWaypointClusterInputs();
|
||||||
|
|
||||||
|
if (!existingWaypoints.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const clusterResponse: WaypointPhotoClusterResponse =
|
||||||
|
await getPb().send("/waypoint/cluster", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
category: $formData.category,
|
||||||
|
photos: [
|
||||||
|
{
|
||||||
|
id: waypointMergeCheckPhotoId,
|
||||||
|
lat: savedWaypoint.lat,
|
||||||
|
lon: savedWaypoint.lon,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
waypoints: existingWaypoints,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const matchingCluster = clusterResponse.clusters.find(
|
||||||
|
(cluster) =>
|
||||||
|
cluster.waypoint &&
|
||||||
|
cluster.photos.includes(waypointMergeCheckPhotoId),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchingCluster?.waypoint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $formData.expand?.waypoints_via_trail?.find(
|
||||||
|
(wp) => wp.id === matchingCluster.waypoint,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
show_toast(
|
||||||
|
{
|
||||||
|
type: "error",
|
||||||
|
icon: "warning",
|
||||||
|
text: $_("waypoint-cluster-error"),
|
||||||
|
},
|
||||||
|
10000,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPendingWaypointAnyway() {
|
||||||
|
if (!pendingWaypointMerge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
commitWaypoint(pendingWaypointMerge.incoming);
|
||||||
|
closeWaypointMergeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function addPendingWaypointToExisting(options: WaypointMergeOptions) {
|
||||||
|
if (!pendingWaypointMerge) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { incoming, existing } = pendingWaypointMerge;
|
||||||
|
const mergedWaypoint = {
|
||||||
|
...existing,
|
||||||
|
icon: options.icon ? incoming.icon : existing.icon,
|
||||||
|
name: options.title
|
||||||
|
? appendDistinctText(existing.name, incoming.name, " / ")
|
||||||
|
: existing.name,
|
||||||
|
description: options.description
|
||||||
|
? appendDistinctText(
|
||||||
|
existing.description,
|
||||||
|
incoming.description,
|
||||||
|
"\n\n",
|
||||||
|
)
|
||||||
|
: existing.description,
|
||||||
|
photos: existing.photos ?? [],
|
||||||
|
_photos: options.photos
|
||||||
|
? [
|
||||||
|
...((existing as Waypoint)._photos ?? []),
|
||||||
|
...(incoming._photos ?? []),
|
||||||
|
]
|
||||||
|
: (existing as Waypoint)._photos,
|
||||||
|
} as Waypoint;
|
||||||
|
|
||||||
|
closeWaypointMergeModal();
|
||||||
|
waypoint.set(mergedWaypoint);
|
||||||
|
waypointModal.openModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendDistinctText(
|
||||||
|
existing: string | undefined,
|
||||||
|
incoming: string | undefined,
|
||||||
|
separator: string,
|
||||||
|
) {
|
||||||
|
const existingText = existing?.trim() ?? "";
|
||||||
|
const incomingText = incoming?.trim() ?? "";
|
||||||
|
|
||||||
|
if (!incomingText || existingText === incomingText) {
|
||||||
|
return existing ?? "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingText) {
|
||||||
|
return incomingText;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${existingText}${separator}${incomingText}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeWaypointMergeModal() {
|
||||||
|
pendingWaypointMerge = undefined;
|
||||||
|
waypointMergeModal.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelPendingWaypointMerge() {
|
||||||
|
if (pendingWaypointMerge) {
|
||||||
|
waypoint.set(pendingWaypointMerge.incoming);
|
||||||
|
}
|
||||||
|
|
||||||
|
closeWaypointMergeModal();
|
||||||
|
waypointModal.openModal();
|
||||||
|
}
|
||||||
|
|
||||||
function moveMarker(marker: M.Marker, wpId?: string) {
|
function moveMarker(marker: M.Marker, wpId?: string) {
|
||||||
const position = marker.getLngLat();
|
const position = marker.getLngLat();
|
||||||
const editableWaypointIndex =
|
const editableWaypointIndex =
|
||||||
@@ -563,7 +737,7 @@
|
|||||||
} else {
|
} else {
|
||||||
list = await lists_add_trail(list, $formData as Trail);
|
list = await lists_add_trail(list, $formData as Trail);
|
||||||
}
|
}
|
||||||
const index = lists.items.findIndex((l) => l.id == list.id);
|
const index = lists.items.findIndex((l: List) => l.id == list.id);
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
lists.items[index] = list;
|
lists.items[index] = list;
|
||||||
}
|
}
|
||||||
@@ -1129,6 +1303,28 @@
|
|||||||
document.getElementById("waypoint-photo-input")!.click();
|
document.getElementById("waypoint-photo-input")!.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface GPXCoord {
|
||||||
|
id: string;
|
||||||
|
longitude: number;
|
||||||
|
latitude: number;
|
||||||
|
file: File;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WaypointPhotoCluster {
|
||||||
|
lat: number;
|
||||||
|
lon: number;
|
||||||
|
waypoint?: string;
|
||||||
|
photos: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WaypointPhotoClusterResponse {
|
||||||
|
mergeEnabled: boolean;
|
||||||
|
mergeRadius: number;
|
||||||
|
clusters: WaypointPhotoCluster[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const waypointMergeCheckPhotoId = "__waypoint_merge_check__";
|
||||||
|
|
||||||
async function handleWaypointPhotoSelection() {
|
async function handleWaypointPhotoSelection() {
|
||||||
const files = (
|
const files = (
|
||||||
document.getElementById("waypoint-photo-input") as HTMLInputElement
|
document.getElementById("waypoint-photo-input") as HTMLInputElement
|
||||||
@@ -1138,8 +1334,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const file of files) {
|
const photoCoords: GPXCoord[] = [];
|
||||||
const coords = await new Promise<number[]>((resolve) => {
|
|
||||||
|
for (const [index, file] of Array.from(files).entries()) {
|
||||||
|
const coords = await new Promise<GPXCoord | undefined>((resolve) => {
|
||||||
EXIF.getData(file, function (p) {
|
EXIF.getData(file, function (p) {
|
||||||
const lat = EXIF.getTag(p, "GPSLatitude");
|
const lat = EXIF.getTag(p, "GPSLatitude");
|
||||||
const latDir = EXIF.getTag(p, "GPSLatitudeRef");
|
const latDir = EXIF.getTag(p, "GPSLatitudeRef");
|
||||||
@@ -1147,22 +1345,19 @@
|
|||||||
const lonDir = EXIF.getTag(p, "GPSLongitudeRef");
|
const lonDir = EXIF.getTag(p, "GPSLongitudeRef");
|
||||||
|
|
||||||
if (lat && lon) {
|
if (lat && lon) {
|
||||||
resolve([
|
resolve({
|
||||||
convertDMSToDD(lat, latDir),
|
id: index.toString(),
|
||||||
convertDMSToDD(lon, lonDir),
|
latitude: convertDMSToDD(lat, latDir),
|
||||||
]);
|
longitude: convertDMSToDD(lon, lonDir),
|
||||||
|
file,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
resolve([]);
|
resolve(undefined);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
if (coords.length) {
|
|
||||||
const wp: Waypoint = new Waypoint(coords[0], coords[1], {
|
if (!coords) {
|
||||||
icon: "image",
|
|
||||||
});
|
|
||||||
wp._photos = [file];
|
|
||||||
saveWaypoint(wp);
|
|
||||||
} else {
|
|
||||||
show_toast(
|
show_toast(
|
||||||
{
|
{
|
||||||
type: "warning",
|
type: "warning",
|
||||||
@@ -1171,7 +1366,80 @@
|
|||||||
},
|
},
|
||||||
10000,
|
10000,
|
||||||
);
|
);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
photoCoords.push(coords);
|
||||||
|
}
|
||||||
|
|
||||||
|
let clusterResponse: WaypointPhotoClusterResponse;
|
||||||
|
try {
|
||||||
|
clusterResponse = await getPb().send("/waypoint/cluster", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
category: $formData.category,
|
||||||
|
photos: photoCoords.map((coords) => ({
|
||||||
|
id: coords.id,
|
||||||
|
lat: coords.latitude,
|
||||||
|
lon: coords.longitude,
|
||||||
|
})),
|
||||||
|
waypoints: getExistingWaypointClusterInputs(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
show_toast(
|
||||||
|
{
|
||||||
|
type: "error",
|
||||||
|
icon: "warning",
|
||||||
|
text: $_("waypoint-cluster-error"),
|
||||||
|
},
|
||||||
|
10000,
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileMap = new Map(photoCoords.map((coords) => [coords.id, coords.file]));
|
||||||
|
|
||||||
|
for (const cluster of clusterResponse.clusters) {
|
||||||
|
const photos = cluster.photos
|
||||||
|
.map((id) => fileMap.get(id))
|
||||||
|
.filter((file): file is File => file != null);
|
||||||
|
|
||||||
|
if (!photos.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cluster.waypoint) {
|
||||||
|
const existingWaypoint =
|
||||||
|
$formData.expand?.waypoints_via_trail?.find(
|
||||||
|
(wp) => wp.id === cluster.waypoint,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingWaypoint) {
|
||||||
|
const existingWaypointPhotos =
|
||||||
|
(existingWaypoint as Waypoint)._photos ?? [];
|
||||||
|
|
||||||
|
commitWaypoint({
|
||||||
|
...existingWaypoint,
|
||||||
|
photos: existingWaypoint.photos ?? [],
|
||||||
|
_photos: [...existingWaypointPhotos, ...photos],
|
||||||
|
} as Waypoint);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const wp: Waypoint = new Waypoint(
|
||||||
|
cluster.lat,
|
||||||
|
cluster.lon,
|
||||||
|
{
|
||||||
|
icon: photos.length > 1 ? "images" : "image",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
wp._photos = photos;
|
||||||
|
commitWaypoint(wp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1546,6 +1814,13 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<WaypointModal bind:this={waypointModal} onsave={saveWaypoint}></WaypointModal>
|
<WaypointModal bind:this={waypointModal} onsave={saveWaypoint}></WaypointModal>
|
||||||
|
<WaypointMergeModal
|
||||||
|
merge={pendingWaypointMerge}
|
||||||
|
bind:this={waypointMergeModal}
|
||||||
|
oncreate={createPendingWaypointAnyway}
|
||||||
|
onmerge={addPendingWaypointToExisting}
|
||||||
|
oncancel={cancelPendingWaypointMerge}
|
||||||
|
></WaypointMergeModal>
|
||||||
<SummitLogModal bind:this={summitLogModal} onsave={(log) => saveSummitLog(log)}
|
<SummitLogModal bind:this={summitLogModal} onsave={(log) => saveSummitLog(log)}
|
||||||
></SummitLogModal>
|
></SummitLogModal>
|
||||||
<ListSearchModal
|
<ListSearchModal
|
||||||
|
|||||||
Reference in New Issue
Block a user