generate name and type for date-only activity names
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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'}`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user