fix: Höhenprofil-Hervorhebung über Distanz statt Index koppeln
elevation enthält einen Eintrag pro Originalpunkt, geojson.coordinates nur die per Douglas-Peucker vereinfachten Punkte. Ein gemeinsamer Index zeigte deshalb ab rund 3 % des Profils auf das Streckenende. Die neue Distanz-Kopplung nutzt coord_distances (kumulative Distanz je vereinfachter Koordinate), um beim Überfahren des Höhenprofils die nächstgelegene Stelle auf der Karte zu finden. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NBMBfip6BAG8VV9raSf6vu
This commit is contained in:
parent
08af521d19
commit
7b1df79a37
10 changed files with 138 additions and 36 deletions
|
|
@ -1260,11 +1260,22 @@ migrate((app) => {
|
||||||
"presentable": false,
|
"presentable": false,
|
||||||
"system": false,
|
"system": false,
|
||||||
"type": "autodate"
|
"type": "autodate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"help": "",
|
||||||
|
"hidden": false,
|
||||||
|
"id": "json1889387860",
|
||||||
|
"maxSize": 2000000,
|
||||||
|
"name": "coord_distances",
|
||||||
|
"presentable": false,
|
||||||
|
"required": false,
|
||||||
|
"system": false,
|
||||||
|
"type": "json"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"indexes": [],
|
"indexes": [],
|
||||||
"created": "2026-08-06 14:55:19.501Z",
|
"created": "2026-08-06 14:55:19.501Z",
|
||||||
"updated": "2026-08-06 14:55:19.501Z",
|
"updated": "2026-08-06 17:20:30.496Z",
|
||||||
"system": false
|
"system": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
/// <reference path="../pb_data/types.d.ts" />
|
||||||
|
|
||||||
|
// Kumulative Distanz je Koordinate der vereinfachten Geometrie. Nötig, weil
|
||||||
|
// elevation einen Eintrag pro Originalpunkt hat, geojson aber nur die
|
||||||
|
// vereinfachten — ohne diese Brücke zeigt die Hervorhebung aus dem
|
||||||
|
// Höhenprofil auf der Karte an die falsche Stelle.
|
||||||
|
|
||||||
|
migrate((app) => {
|
||||||
|
const c = app.findCollectionByNameOrId('trail_versions')
|
||||||
|
c.fields.add(new Field({
|
||||||
|
name: 'coord_distances',
|
||||||
|
type: 'json',
|
||||||
|
maxSize: 2000000,
|
||||||
|
}))
|
||||||
|
app.save(c)
|
||||||
|
}, (app) => {
|
||||||
|
const c = app.findCollectionByNameOrId('trail_versions')
|
||||||
|
c.fields.removeByName('coord_distances')
|
||||||
|
app.save(c)
|
||||||
|
})
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
/**
|
/**
|
||||||
* Höhenprofil als SVG-Fläche. Beim Überfahren meldet die Komponente den
|
* Höhenprofil als SVG-Fläche. Beim Überfahren meldet die Komponente die
|
||||||
* Index des Punktes; die Elternkomponente reicht ihn an die Karte weiter,
|
* Distanz des Punktes; die Elternkomponente reicht sie an die Karte
|
||||||
* damit dort dieselbe Stelle hervorgehoben wird.
|
* weiter, damit dort dieselbe Stelle hervorgehoben wird. Distanz statt
|
||||||
|
* Index, weil die Karte eine andere (vereinfachte) Punktreihe zeigt.
|
||||||
*/
|
*/
|
||||||
let {
|
let {
|
||||||
elevation,
|
elevation,
|
||||||
|
|
@ -10,7 +11,7 @@
|
||||||
height = '120px',
|
height = '120px',
|
||||||
}: {
|
}: {
|
||||||
elevation: { d: number; ele: number }[]
|
elevation: { d: number; ele: number }[]
|
||||||
onhover?: (index: number | null) => void
|
onhover?: (distance: number | null) => void
|
||||||
height?: string
|
height?: string
|
||||||
} = $props()
|
} = $props()
|
||||||
|
|
||||||
|
|
@ -56,7 +57,7 @@
|
||||||
const clamped = Math.max(0, Math.min(elevation.length - 1, idx))
|
const clamped = Math.max(0, Math.min(elevation.length - 1, idx))
|
||||||
|
|
||||||
hoverX = ratio * W
|
hoverX = ratio * W
|
||||||
onhover?.(clamped)
|
onhover?.(elevation[clamped].d)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLeave() {
|
function handleLeave() {
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,8 @@
|
||||||
markers = [],
|
markers = [],
|
||||||
status = 'offen',
|
status = 'offen',
|
||||||
placing = false,
|
placing = false,
|
||||||
highlightIndex = null,
|
coordDistances = [],
|
||||||
|
highlightDistance = null,
|
||||||
onplace,
|
onplace,
|
||||||
onmarkerclick,
|
onmarkerclick,
|
||||||
height = '420px',
|
height = '420px',
|
||||||
|
|
@ -35,7 +36,8 @@
|
||||||
markers?: MarkerData[]
|
markers?: MarkerData[]
|
||||||
status?: 'offen' | 'eingeschraenkt' | 'gesperrt'
|
status?: 'offen' | 'eingeschraenkt' | 'gesperrt'
|
||||||
placing?: boolean
|
placing?: boolean
|
||||||
highlightIndex?: number | null
|
coordDistances?: number[]
|
||||||
|
highlightDistance?: number | null
|
||||||
onplace?: (coords: { lat: number; lng: number }) => void
|
onplace?: (coords: { lat: number; lng: number }) => void
|
||||||
onmarkerclick?: (id: string) => void
|
onmarkerclick?: (id: string) => void
|
||||||
height?: string
|
height?: string
|
||||||
|
|
@ -206,7 +208,9 @@
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// Stelle aus dem Höhenprofil hervorheben
|
// Stelle aus dem Höhenprofil hervorheben. Die Kopplung läuft über die
|
||||||
|
// Distanz, nicht über den Index: elevation hat einen Eintrag pro
|
||||||
|
// Originalpunkt, coordinates nur die vereinfachten.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (!map || !ready) return
|
if (!map || !ready) return
|
||||||
|
|
||||||
|
|
@ -214,9 +218,20 @@
|
||||||
highlightMarker = null
|
highlightMarker = null
|
||||||
|
|
||||||
const coords = geojson?.coordinates
|
const coords = geojson?.coordinates
|
||||||
if (highlightIndex == null || !coords?.length) return
|
if (highlightDistance == null || !coords?.length || !coordDistances.length) return
|
||||||
|
|
||||||
const idx = Math.max(0, Math.min(coords.length - 1, highlightIndex))
|
// Nächstgelegene Koordinate zur gesuchten Distanz
|
||||||
|
let best = 0
|
||||||
|
let bestDiff = Infinity
|
||||||
|
for (let i = 0; i < coordDistances.length; i++) {
|
||||||
|
const diff = Math.abs(coordDistances[i] - highlightDistance)
|
||||||
|
if (diff < bestDiff) {
|
||||||
|
bestDiff = diff
|
||||||
|
best = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const idx = Math.min(coords.length - 1, best)
|
||||||
const el = document.createElement('div')
|
const el = document.createElement('div')
|
||||||
el.style.cssText = `
|
el.style.cssText = `
|
||||||
width: 12px; height: 12px; border-radius: 9999px;
|
width: 12px; height: 12px; border-radius: 9999px;
|
||||||
|
|
|
||||||
|
|
@ -171,3 +171,21 @@ describe('simplify', () => {
|
||||||
expect(out[out.length - 1]).toEqual(pts[pts.length - 1])
|
expect(out[out.length - 1]).toEqual(pts[pts.length - 1])
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe('coordDistances', () => {
|
||||||
|
it('hat dieselbe Länge wie die Koordinaten', () => {
|
||||||
|
let pts = ''
|
||||||
|
for (let i = 0; i < 5000; i++) {
|
||||||
|
pts += `<trkpt lat="${49.5 + i * 0.00002}" lon="${11.4 + Math.sin(i / 300) * 0.01}"><ele>${300 + i * 0.01}</ele></trkpt>`
|
||||||
|
}
|
||||||
|
const r = parseGpx(`<?xml version="1.0"?><gpx version="1.1" xmlns="http://www.topografix.com/GPX/1/1"><trk><trkseg>${pts}</trkseg></trk></gpx>`)
|
||||||
|
|
||||||
|
expect(r.coordDistances).toHaveLength(r.geojson.coordinates.length)
|
||||||
|
// monoton steigend, beginnt bei 0, endet bei der Gesamtlänge
|
||||||
|
expect(r.coordDistances[0]).toBe(0)
|
||||||
|
expect(r.coordDistances[r.coordDistances.length - 1]).toBeCloseTo(r.distance_m, -1)
|
||||||
|
for (let i = 1; i < r.coordDistances.length; i++) {
|
||||||
|
expect(r.coordDistances[i]).toBeGreaterThanOrEqual(r.coordDistances[i - 1])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ export type TrackPoint = { lat: number; lng: number; ele: number | null }
|
||||||
export type ParsedGpx = {
|
export type ParsedGpx = {
|
||||||
points: TrackPoint[]
|
points: TrackPoint[]
|
||||||
geojson: { type: 'LineString'; coordinates: [number, number][] }
|
geojson: { type: 'LineString'; coordinates: [number, number][] }
|
||||||
|
/** Kumulative Distanz je Koordinate in `geojson.coordinates`, in Metern. */
|
||||||
|
coordDistances: number[]
|
||||||
elevation: { d: number; ele: number }[]
|
elevation: { d: number; ele: number }[]
|
||||||
bounds: [[number, number], [number, number]]
|
bounds: [[number, number], [number, number]]
|
||||||
distance_m: number
|
distance_m: number
|
||||||
|
|
@ -49,38 +51,48 @@ export function haversine(a: TrackPoint, b: TrackPoint): number {
|
||||||
* beherrschbar wird.
|
* beherrschbar wird.
|
||||||
*/
|
*/
|
||||||
export function simplify(points: TrackPoint[], maxPoints = 2000): TrackPoint[] {
|
export function simplify(points: TrackPoint[], maxPoints = 2000): TrackPoint[] {
|
||||||
if (points.length <= maxPoints) return points
|
return simplifyIndices(points, maxPoints).map((i) => points[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wie `simplify`, liefert aber die Indizes der erhaltenen Punkte statt der
|
||||||
|
* Punkte selbst. Damit lässt sich zu jeder vereinfachten Koordinate die
|
||||||
|
* ursprüngliche Distanz nachschlagen.
|
||||||
|
*/
|
||||||
|
export function simplifyIndices(points: TrackPoint[], maxPoints = 2000): number[] {
|
||||||
|
if (points.length <= maxPoints) {
|
||||||
|
return points.map((_, i) => i)
|
||||||
|
}
|
||||||
|
|
||||||
let tolerance = 0.00001
|
let tolerance = 0.00001
|
||||||
let out = points
|
let out = points.map((_, i) => i)
|
||||||
|
|
||||||
// Obergrenze gegen Endlosschleifen bei entarteten Daten
|
|
||||||
for (let i = 0; i < 30 && out.length > maxPoints; i++) {
|
for (let i = 0; i < 30 && out.length > maxPoints; i++) {
|
||||||
out = douglasPeucker(points, tolerance)
|
out = douglasPeuckerIndices(points, points.map((_, k) => k), tolerance)
|
||||||
tolerance *= 2
|
tolerance *= 2
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
function douglasPeucker(pts: TrackPoint[], tolerance: number): TrackPoint[] {
|
function douglasPeuckerIndices(all: TrackPoint[], idx: number[], tolerance: number): number[] {
|
||||||
if (pts.length < 3) return pts
|
if (idx.length < 3) return idx
|
||||||
|
|
||||||
let maxDist = 0
|
let maxDist = 0
|
||||||
let index = 0
|
let split = 0
|
||||||
|
|
||||||
for (let i = 1; i < pts.length - 1; i++) {
|
for (let i = 1; i < idx.length - 1; i++) {
|
||||||
const d = perpendicularDistance(pts[i], pts[0], pts[pts.length - 1])
|
const d = perpendicularDistance(all[idx[i]], all[idx[0]], all[idx[idx.length - 1]])
|
||||||
if (d > maxDist) {
|
if (d > maxDist) {
|
||||||
maxDist = d
|
maxDist = d
|
||||||
index = i
|
split = i
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (maxDist <= tolerance) return [pts[0], pts[pts.length - 1]]
|
if (maxDist <= tolerance) return [idx[0], idx[idx.length - 1]]
|
||||||
|
|
||||||
const left = douglasPeucker(pts.slice(0, index + 1), tolerance)
|
const left = douglasPeuckerIndices(all, idx.slice(0, split + 1), tolerance)
|
||||||
const right = douglasPeucker(pts.slice(index), tolerance)
|
const right = douglasPeuckerIndices(all, idx.slice(split), tolerance)
|
||||||
return [...left.slice(0, -1), ...right]
|
return [...left.slice(0, -1), ...right]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,10 +169,16 @@ export function parseGpx(xml: string): ParsedGpx {
|
||||||
const elevation: { d: number; ele: number }[] = []
|
const elevation: { d: number; ele: number }[] = []
|
||||||
let lastCountedEle: number | null = points[0].ele
|
let lastCountedEle: number | null = points[0].ele
|
||||||
|
|
||||||
|
// Kumulative Distanz je Originalpunkt — Brücke zwischen elevation (ein
|
||||||
|
// Eintrag pro Originalpunkt) und der vereinfachten Geometrie.
|
||||||
|
const distAt: number[] = new Array(points.length)
|
||||||
|
distAt[0] = 0
|
||||||
|
|
||||||
if (points[0].ele != null) elevation.push({ d: 0, ele: points[0].ele })
|
if (points[0].ele != null) elevation.push({ d: 0, ele: points[0].ele })
|
||||||
|
|
||||||
for (let i = 1; i < points.length; i++) {
|
for (let i = 1; i < points.length; i++) {
|
||||||
distance += haversine(points[i - 1], points[i])
|
distance += haversine(points[i - 1], points[i])
|
||||||
|
distAt[i] = distance
|
||||||
|
|
||||||
const ele = points[i].ele
|
const ele = points[i].ele
|
||||||
if (ele == null) continue
|
if (ele == null) continue
|
||||||
|
|
@ -185,7 +203,8 @@ export function parseGpx(xml: string): ParsedGpx {
|
||||||
const lats = points.map((p) => p.lat)
|
const lats = points.map((p) => p.lat)
|
||||||
const lngs = points.map((p) => p.lng)
|
const lngs = points.map((p) => p.lng)
|
||||||
|
|
||||||
const simplified = simplify(points)
|
const keep = simplifyIndices(points)
|
||||||
|
const simplified = keep.map((i) => points[i])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
points: simplified,
|
points: simplified,
|
||||||
|
|
@ -193,6 +212,7 @@ export function parseGpx(xml: string): ParsedGpx {
|
||||||
type: 'LineString',
|
type: 'LineString',
|
||||||
coordinates: simplified.map((p) => [p.lng, p.lat] as [number, number]),
|
coordinates: simplified.map((p) => [p.lng, p.lat] as [number, number]),
|
||||||
},
|
},
|
||||||
|
coordDistances: keep.map((i) => Math.round(distAt[i])),
|
||||||
elevation,
|
elevation,
|
||||||
bounds: [
|
bounds: [
|
||||||
[Math.min(...lngs), Math.min(...lats)],
|
[Math.min(...lngs), Math.min(...lats)],
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ export class TrailVersionStore {
|
||||||
form.append('trail', trailId)
|
form.append('trail', trailId)
|
||||||
form.append('gpx', file)
|
form.append('gpx', file)
|
||||||
form.append('geojson', JSON.stringify(parsed.geojson))
|
form.append('geojson', JSON.stringify(parsed.geojson))
|
||||||
|
form.append('coord_distances', JSON.stringify(parsed.coordDistances))
|
||||||
form.append('elevation', JSON.stringify(parsed.elevation))
|
form.append('elevation', JSON.stringify(parsed.elevation))
|
||||||
form.append('bounds', JSON.stringify(parsed.bounds))
|
form.append('bounds', JSON.stringify(parsed.bounds))
|
||||||
form.append('distance_m', String(parsed.distance_m))
|
form.append('distance_m', String(parsed.distance_m))
|
||||||
|
|
|
||||||
5
frontend/src/lib/types.d.ts
vendored
5
frontend/src/lib/types.d.ts
vendored
|
|
@ -158,9 +158,10 @@ export type TrailMarkersRecord = {
|
||||||
updated?: IsoDateString
|
updated?: IsoDateString
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TrailVersionsRecord<Tbounds = unknown, Televation = unknown, Tgeojson = unknown> = {
|
export type TrailVersionsRecord<Tbounds = unknown, Tcoord_distances = unknown, Televation = unknown, Tgeojson = unknown> = {
|
||||||
ascent_m?: number
|
ascent_m?: number
|
||||||
bounds?: null | Tbounds
|
bounds?: null | Tbounds
|
||||||
|
coord_distances?: null | Tcoord_distances
|
||||||
created?: IsoDateString
|
created?: IsoDateString
|
||||||
distance_m?: number
|
distance_m?: number
|
||||||
elevation?: null | Televation
|
elevation?: null | Televation
|
||||||
|
|
@ -215,7 +216,7 @@ export type TimesResponse<Texpand = unknown> = Required<TimesRecord> & BaseSyste
|
||||||
export type TrailCommentsResponse<Texpand = unknown> = Required<TrailCommentsRecord> & BaseSystemFields<Texpand>
|
export type TrailCommentsResponse<Texpand = unknown> = Required<TrailCommentsRecord> & BaseSystemFields<Texpand>
|
||||||
export type TrailFlagsResponse<Texpand = unknown> = Required<TrailFlagsRecord> & BaseSystemFields<Texpand>
|
export type TrailFlagsResponse<Texpand = unknown> = Required<TrailFlagsRecord> & BaseSystemFields<Texpand>
|
||||||
export type TrailMarkersResponse<Texpand = unknown> = Required<TrailMarkersRecord> & BaseSystemFields<Texpand>
|
export type TrailMarkersResponse<Texpand = unknown> = Required<TrailMarkersRecord> & BaseSystemFields<Texpand>
|
||||||
export type TrailVersionsResponse<Tbounds = unknown, Televation = unknown, Tgeojson = unknown, Texpand = unknown> = Required<TrailVersionsRecord<Tbounds, Televation, Tgeojson>> & BaseSystemFields<Texpand>
|
export type TrailVersionsResponse<Tbounds = unknown, Tcoord_distances = unknown, Televation = unknown, Tgeojson = unknown, Texpand = unknown> = Required<TrailVersionsRecord<Tbounds, Tcoord_distances, Televation, Tgeojson>> & BaseSystemFields<Texpand>
|
||||||
export type TrailsResponse<Texpand = unknown> = Required<TrailsRecord> & BaseSystemFields<Texpand>
|
export type TrailsResponse<Texpand = unknown> = Required<TrailsRecord> & BaseSystemFields<Texpand>
|
||||||
export type UsersResponse<Texpand = unknown> = Required<UsersRecord> & AuthSystemFields<Texpand>
|
export type UsersResponse<Texpand = unknown> = Required<UsersRecord> & AuthSystemFields<Texpand>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
function currentVersion(trailId: string): TrailVersionsResponse<
|
function currentVersion(trailId: string): TrailVersionsResponse<
|
||||||
[[number, number], [number, number]] | null,
|
[[number, number], [number, number]] | null,
|
||||||
unknown,
|
unknown,
|
||||||
|
unknown,
|
||||||
LineGeoJSON | null
|
LineGeoJSON | null
|
||||||
> | null {
|
> | null {
|
||||||
const trail = trails.getById(trailId)
|
const trail = trails.getById(trailId)
|
||||||
|
|
@ -47,6 +48,7 @@
|
||||||
return (versions.records.find((v) => v.id === trail.current) ?? null) as TrailVersionsResponse<
|
return (versions.records.find((v) => v.id === trail.current) ?? null) as TrailVersionsResponse<
|
||||||
[[number, number], [number, number]] | null,
|
[[number, number], [number, number]] | null,
|
||||||
unknown,
|
unknown,
|
||||||
|
unknown,
|
||||||
LineGeoJSON | null
|
LineGeoJSON | null
|
||||||
> | null
|
> | null
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
import { getTrailVersionContext } from '$lib/stores/trailVersions.svelte'
|
import { getTrailVersionContext } from '$lib/stores/trailVersions.svelte'
|
||||||
import { getTrailFlagContext } from '$lib/stores/trailFlags.svelte'
|
import { getTrailFlagContext } from '$lib/stores/trailFlags.svelte'
|
||||||
import { getTrailMarkerContext } from '$lib/stores/trailMarkers.svelte'
|
import { getTrailMarkerContext } from '$lib/stores/trailMarkers.svelte'
|
||||||
|
import { getTeamContext } from '$lib/stores/teams.svelte'
|
||||||
import TrailMap from '$lib/components/TrailMap.svelte'
|
import TrailMap from '$lib/components/TrailMap.svelte'
|
||||||
import ElevationProfile from '$lib/components/ElevationProfile.svelte'
|
import ElevationProfile from '$lib/components/ElevationProfile.svelte'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
@ -30,6 +31,7 @@
|
||||||
type Elevation = { d: number; ele: number }[]
|
type Elevation = { d: number; ele: number }[]
|
||||||
type Version = TrailVersionsResponse<
|
type Version = TrailVersionsResponse<
|
||||||
[[number, number], [number, number]] | null,
|
[[number, number], [number, number]] | null,
|
||||||
|
number[] | null,
|
||||||
Elevation | null,
|
Elevation | null,
|
||||||
LineGeoJSON | null
|
LineGeoJSON | null
|
||||||
>
|
>
|
||||||
|
|
@ -38,9 +40,11 @@
|
||||||
const versions = getTrailVersionContext()
|
const versions = getTrailVersionContext()
|
||||||
const flags = getTrailFlagContext()
|
const flags = getTrailFlagContext()
|
||||||
const markers = getTrailMarkerContext()
|
const markers = getTrailMarkerContext()
|
||||||
|
const teams = getTeamContext()
|
||||||
|
|
||||||
const trail = $derived(page.params.id ? trails.getById(page.params.id) : undefined)
|
const trail = $derived(page.params.id ? trails.getById(page.params.id) : undefined)
|
||||||
const canEdit = $derived(trail ? trails.canEdit(trail) : false)
|
const canEdit = $derived(trail ? trails.canEdit(trail) : false)
|
||||||
|
const team = $derived(trail ? teams.records.find((t) => t.id === trail.team) : undefined)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* description ist ein editor-Feld und enthält HTML. Gesetzt wird es zwar
|
* description ist ein editor-Feld und enthält HTML. Gesetzt wird es zwar
|
||||||
|
|
@ -287,7 +291,8 @@
|
||||||
markers={mapMarkers}
|
markers={mapMarkers}
|
||||||
status={trail.status}
|
status={trail.status}
|
||||||
{placing}
|
{placing}
|
||||||
highlightIndex={highlight}
|
coordDistances={version.coord_distances ?? []}
|
||||||
|
highlightDistance={highlight}
|
||||||
onplace={onPlace}
|
onplace={onPlace}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
@ -388,14 +393,22 @@
|
||||||
{/if}
|
{/if}
|
||||||
<p class="text-xs text-muted-foreground">{formatDate(m.created)}</p>
|
<p class="text-xs text-muted-foreground">{formatDate(m.created)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
{#if markers.canEdit(m, (trail.stewards ?? []) as string[], team?.owner, (team?.admins ?? []) as string[])}
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
title={m.resolved ? 'Wieder öffnen' : 'Als erledigt markieren'}
|
title={m.resolved ? 'Wieder öffnen' : 'Als erledigt markieren'}
|
||||||
onclick={() => markers.toggleResolved(m.id)}
|
onclick={async () => {
|
||||||
|
try {
|
||||||
|
await markers.toggleResolved(m.id)
|
||||||
|
} catch (e: any) {
|
||||||
|
alert(e.message ?? 'Der Marker konnte nicht geändert werden.')
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<Check class="size-4" />
|
<Check class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
{/if}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue