From cc9037c9223a9f75f41614e5e1960125d742136f Mon Sep 17 00:00:00 2001 From: Vincent van der Wal Date: Wed, 22 Jul 2026 12:24:49 +0200 Subject: [PATCH] generate name and type for date-only activity names --- src/lib/server/gpx.ts | 3 ++ src/routes/upload/+page.server.ts | 63 +++++++++++++++++++++++++++---- 2 files changed, 59 insertions(+), 7 deletions(-) diff --git a/src/lib/server/gpx.ts b/src/lib/server/gpx.ts index e9281b5..2544627 100644 --- a/src/lib/server/gpx.ts +++ b/src/lib/server/gpx.ts @@ -12,6 +12,8 @@ export interface ParsedGpx { name: string | null; type: string | null; date: string | null; + /** ISO timestamp of the first trackpoint, when the GPX has times */ + start: string | null; points: TrackPoint[]; distance_m: number; duration_s: number | null; @@ -149,6 +151,7 @@ export function parseGpx(xml: string): ParsedGpx { name: trk0?.name ? String(trk0.name) : meta?.name ? String(meta.name) : null, type: trk0?.type ? String(trk0.type).toLowerCase() : null, date, + start: firstTime !== null ? new Date(firstTime).toISOString() : null, points: simplify(points, 2500), distance_m: distance, duration_s: duration, diff --git a/src/routes/upload/+page.server.ts b/src/routes/upload/+page.server.ts index 7dc3a47..ed71155 100644 --- a/src/routes/upload/+page.server.ts +++ b/src/routes/upload/+page.server.ts @@ -3,6 +3,47 @@ import { db, peaksNearBounds, recordAscent } from '$lib/server/db'; import { matchPeaks, parseGpx } from '$lib/server/gpx'; import type { Actions } from './$types'; +/** A "name" that is really just a date or timestamp, e.g. "2023-10-03 17:39:35". */ +const DATE_ONLY_NAME = + /^\d{4}[-/.]\d{1,2}[-/.]\d{1,2}([ T](\d{1,2}[:.]\d{2}([:.]\d{2})?|\d{4,6}))?Z?$/; + +/** Infer an activity type from average moving speed when the GPX doesn't say. */ +function inferType(gpx: { distance_m: number; moving_s: number | null }): string | null { + if (!gpx.moving_s || gpx.moving_s < 60) return null; + const kmh = (gpx.distance_m / gpx.moving_s) * 3.6; + if (kmh >= 13) return 'ride'; + if (kmh >= 7) return 'run'; + return 'hike'; +} + +/** "Morning Hike", "Evening Run", or "Hike on 3 Oct 2023" when there's no start time. */ +function generateName(type: string, start: string | null, date: string | null): string { + const capitalized = type.charAt(0).toUpperCase() + type.slice(1); + if (start) { + const hour = new Date(start).getUTCHours(); + const period = + hour >= 4 && hour < 11 + ? 'Morning' + : hour >= 11 && hour < 14 + ? 'Lunch' + : hour >= 14 && hour < 18 + ? 'Afternoon' + : hour >= 18 && hour < 22 + ? 'Evening' + : 'Night'; + return `${period} ${capitalized}`; + } + if (date) { + const nice = new Date(date + 'T12:00:00').toLocaleDateString('en-GB', { + day: 'numeric', + month: 'short', + year: 'numeric' + }); + return `${capitalized} on ${nice}`; + } + return capitalized; +} + /** * Parse export-style filenames like * "2018-01-11 141322 - Run - Afternoon Run.gpx" (Date - Type - Name). @@ -45,10 +86,22 @@ export const actions: Actions = { try { const gpx = parseGpx(await file.text()); const parsed = parseFilename(file.name); + + // a missing, "outdoor", or numeric (old Strava code) type gets inferred from pace + let type = parsed?.type ?? gpx.type ?? ''; + if (!type || type === 'outdoor' || /^\d+$/.test(type)) { + type = inferType(gpx) ?? 'outdoor'; + } + // a name that is just a date/timestamp gets a friendly generated one + let name = parsed?.name ?? gpx.name ?? file.name.replace(/\.gpx$/i, ''); + if (!name.trim() || DATE_ONLY_NAME.test(name.trim())) { + name = generateName(type, gpx.start, gpx.date ?? parsed?.date ?? null); + } + const result = insert.run({ user_id: userId, - name: parsed?.name ?? gpx.name ?? file.name.replace(/\.gpx$/i, ''), - type: parsed?.type ?? gpx.type ?? 'outdoor', + name, + type, date: gpx.date ?? parsed?.date ?? null, distance_m: gpx.distance_m, duration_s: gpx.duration_s, @@ -69,11 +122,7 @@ export const actions: Actions = { if (recordAscent(userId, peak.id, activityId, gpx.date)) newPeaks.push(peak.name); } - uploaded.push({ - id: activityId, - name: parsed?.name ?? gpx.name ?? file.name, - newPeaks - }); + uploaded.push({ id: activityId, name, newPeaks }); } catch (err) { errors.push(`${file.name}: ${err instanceof Error ? err.message : 'could not parse'}`); }