generate name and type for date-only activity names
This commit is contained in:
@@ -12,6 +12,8 @@ export interface ParsedGpx {
|
|||||||
name: string | null;
|
name: string | null;
|
||||||
type: string | null;
|
type: string | null;
|
||||||
date: string | null;
|
date: string | null;
|
||||||
|
/** ISO timestamp of the first trackpoint, when the GPX has times */
|
||||||
|
start: string | null;
|
||||||
points: TrackPoint[];
|
points: TrackPoint[];
|
||||||
distance_m: number;
|
distance_m: number;
|
||||||
duration_s: number | null;
|
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,
|
name: trk0?.name ? String(trk0.name) : meta?.name ? String(meta.name) : null,
|
||||||
type: trk0?.type ? String(trk0.type).toLowerCase() : null,
|
type: trk0?.type ? String(trk0.type).toLowerCase() : null,
|
||||||
date,
|
date,
|
||||||
|
start: firstTime !== null ? new Date(firstTime).toISOString() : null,
|
||||||
points: simplify(points, 2500),
|
points: simplify(points, 2500),
|
||||||
distance_m: distance,
|
distance_m: distance,
|
||||||
duration_s: duration,
|
duration_s: duration,
|
||||||
|
|||||||
@@ -3,6 +3,47 @@ import { db, peaksNearBounds, recordAscent } from '$lib/server/db';
|
|||||||
import { matchPeaks, parseGpx } from '$lib/server/gpx';
|
import { matchPeaks, parseGpx } from '$lib/server/gpx';
|
||||||
import type { Actions } from './$types';
|
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
|
* Parse export-style filenames like
|
||||||
* "2018-01-11 141322 - Run - Afternoon Run.gpx" (Date - Type - Name).
|
* "2018-01-11 141322 - Run - Afternoon Run.gpx" (Date - Type - Name).
|
||||||
@@ -45,10 +86,22 @@ export const actions: Actions = {
|
|||||||
try {
|
try {
|
||||||
const gpx = parseGpx(await file.text());
|
const gpx = parseGpx(await file.text());
|
||||||
const parsed = parseFilename(file.name);
|
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({
|
const result = insert.run({
|
||||||
user_id: userId,
|
user_id: userId,
|
||||||
name: parsed?.name ?? gpx.name ?? file.name.replace(/\.gpx$/i, ''),
|
name,
|
||||||
type: parsed?.type ?? gpx.type ?? 'outdoor',
|
type,
|
||||||
date: gpx.date ?? parsed?.date ?? null,
|
date: gpx.date ?? parsed?.date ?? null,
|
||||||
distance_m: gpx.distance_m,
|
distance_m: gpx.distance_m,
|
||||||
duration_s: gpx.duration_s,
|
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);
|
if (recordAscent(userId, peak.id, activityId, gpx.date)) newPeaks.push(peak.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
uploaded.push({
|
uploaded.push({ id: activityId, name, newPeaks });
|
||||||
id: activityId,
|
|
||||||
name: parsed?.name ?? gpx.name ?? file.name,
|
|
||||||
newPeaks
|
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
errors.push(`${file.name}: ${err instanceof Error ? err.message : 'could not parse'}`);
|
errors.push(`${file.name}: ${err instanceof Error ? err.message : 'could not parse'}`);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user