Naprawa obsługi GPX, czyszczenie projektu, naprawa błędów związanych z obrazami

This commit is contained in:
Arek Bykowski
2026-02-15 19:03:02 +01:00
parent 981ce1d1b2
commit 144e28e4c4
10 changed files with 258 additions and 201 deletions

View File

@@ -1,6 +1,12 @@
import { ActivityStats } from '../types';
export const parseGpxFile = async (file: File): Promise<ActivityStats> => {
interface GpxAnalysisResult {
stats: ActivityStats;
richSummary: string;
}
export const parseGpxFile = async (file: File): Promise<GpxAnalysisResult> => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
@@ -13,54 +19,180 @@ export const parseGpxFile = async (file: File): Promise<ActivityStats> => {
const trkpts = Array.from(xmlDoc.getElementsByTagName('trkpt'));
if (trkpts.length === 0) {
// Fallback for rtept if no trkpt
const rtepts = Array.from(xmlDoc.getElementsByTagName('rtept'));
if (rtepts.length === 0) {
throw new Error('No track points found in GPX');
}
// Fallback logic could be added here for routes without time, but for storytelling we focus on tracks
throw new Error('Nie znaleziono punktów śledzenia (trkpt) w pliku GPX.');
}
let totalDistance = 0;
let totalTime = 0;
let elevationGain = 0;
// --- ZMIENNE AGREGUJĄCE ---
let totalDistanceKm = 0;
let totalElevationGain = 0;
let maxElevation = -Infinity;
let minElevation = Infinity;
let startTime: Date | null = null;
let endTime: Date | null = null;
// Heart Rate & Cadence
let totalHr = 0;
let hrCount = 0;
let maxHr = 0;
let totalCadence = 0;
let cadenceCount = 0;
// Splits (KM) Logic
const splits: { km: number; timeMs: number; avgHr: number; elevGain: number }[] = [];
let currentSplitDistance = 0;
let currentSplitTimeStart = 0;
let currentSplitHrSum = 0;
let currentSplitHrCount = 0;
let currentSplitElevGain = 0;
let splitIndex = 1;
let lastLat: number | null = null;
let lastLon: number | null = null;
let lastEle: number | null = null;
let lastTime: number | null = null;
// --- ITERACJA PO PUNKTACH ---
for (let i = 0; i < trkpts.length; i++) {
const lat1 = parseFloat(trkpts[i].getAttribute('lat') || '0');
const lon1 = parseFloat(trkpts[i].getAttribute('lon') || '0');
const ele = parseFloat(trkpts[i].getElementsByTagName('ele')[0]?.textContent || '0');
const timeStr = trkpts[i].getElementsByTagName('time')[0]?.textContent;
const pt = trkpts[i];
const lat = parseFloat(pt.getAttribute('lat') || '0');
const lon = parseFloat(pt.getAttribute('lon') || '0');
const ele = parseFloat(pt.getElementsByTagName('ele')[0]?.textContent || '0');
const timeStr = pt.getElementsByTagName('time')[0]?.textContent;
// Extensions (HR / Cadence)
// Note: Namespaces can vary (ns3:hr, gpxtpx:hr), so we search loosely or by standard tag names inside extensions
const extensions = pt.getElementsByTagName('extensions')[0];
let hr = 0;
let cad = 0;
if (extensions) {
// Try standard Garmin/GPX schemes
const hrNode = extensions.getElementsByTagName('gpxtpx:hr')[0] || extensions.getElementsByTagName('ns3:hr')[0] || extensions.getElementsByTagName('hr')[0];
const cadNode = extensions.getElementsByTagName('gpxtpx:cad')[0] || extensions.getElementsByTagName('ns3:cad')[0] || extensions.getElementsByTagName('cad')[0];
if (hrNode?.textContent) {
hr = parseInt(hrNode.textContent);
if (!isNaN(hr) && hr > 0) {
totalHr += hr;
hrCount++;
if (hr > maxHr) maxHr = hr;
currentSplitHrSum += hr;
currentSplitHrCount++;
}
}
if (cadNode?.textContent) {
cad = parseInt(cadNode.textContent);
if (!isNaN(cad) && cad > 0) {
totalCadence += cad;
cadenceCount++;
}
}
}
if (i > 0) {
const lat2 = parseFloat(trkpts[i - 1].getAttribute('lat') || '0');
const lon2 = parseFloat(trkpts[i - 1].getAttribute('lon') || '0');
totalDistance += getDistanceFromLatLonInKm(lat1, lon1, lat2, lon2);
let currentTime = 0;
if (timeStr) {
const d = new Date(timeStr);
currentTime = d.getTime();
if (!startTime) {
startTime = d;
currentSplitTimeStart = currentTime;
}
endTime = d;
}
// Elevation gain
if (lastEle !== null && ele > lastEle) {
elevationGain += (ele - lastEle);
// Elevation Min/Max
if (!isNaN(ele)) {
if (ele > maxElevation) maxElevation = ele;
if (ele < minElevation) minElevation = ele;
}
// Distance & Calculation
if (lastLat !== null && lastLon !== null) {
const distKm = getDistanceFromLatLonInKm(lastLat, lastLon, lat, lon);
totalDistanceKm += distKm;
currentSplitDistance += distKm;
// Elevation Gain
if (lastEle !== null && !isNaN(ele) && ele > lastEle) {
const gain = ele - lastEle;
totalElevationGain += gain;
currentSplitElevGain += gain;
}
// --- SPLIT LOGIC (Co 1 KM) ---
if (currentSplitDistance >= 1.0) {
const splitTimeMs = currentTime - currentSplitTimeStart;
splits.push({
km: splitIndex,
timeMs: splitTimeMs,
avgHr: currentSplitHrCount > 0 ? Math.round(currentSplitHrSum / currentSplitHrCount) : 0,
elevGain: Math.round(currentSplitElevGain)
});
// Reset Split
splitIndex++;
currentSplitDistance = 0; // or subtract 1.0 specifically for precision, but reset is safer for GPS drift
currentSplitTimeStart = currentTime;
currentSplitHrSum = 0;
currentSplitHrCount = 0;
currentSplitElevGain = 0;
}
}
if (timeStr) {
const time = new Date(timeStr);
if (!startTime) startTime = time;
endTime = time;
}
if (!isNaN(ele)) lastEle = ele;
lastLat = lat;
lastLon = lon;
lastEle = isNaN(ele) ? lastEle : ele;
lastTime = currentTime;
}
if (startTime && endTime) {
totalTime = (endTime.getTime() - startTime.getTime()); // ms
// --- PODSUMOWANIE ---
const totalDurationMs = startTime && endTime ? (endTime.getTime() - startTime.getTime()) : 0;
const avgHr = hrCount > 0 ? Math.round(totalHr / hrCount) : 0;
const avgCadence = cadenceCount > 0 ? Math.round(totalCadence / cadenceCount) : 0;
const avgPaceMs = totalDistanceKm > 0 ? totalDurationMs / totalDistanceKm : 0; // ms per km
// Formatowanie statystyk do UI
const uiStats: ActivityStats = {
distance: `${totalDistanceKm.toFixed(2)} km`,
duration: msToTime(totalDurationMs),
elevation: `${Math.round(totalElevationGain)}m (Max: ${Math.round(maxElevation)}m)`
};
// --- BUDOWANIE BOGATEGO RAPORTU DLA AI ---
let report = `=== SZCZEGÓŁOWY RAPORT Z PLIKU GPX ===\n`;
report += `PODSUMOWANIE:\n`;
report += `- Dystans Całkowity: ${totalDistanceKm.toFixed(2)} km\n`;
report += `- Czas Całkowity: ${msToTime(totalDurationMs)}\n`;
report += `- Średnie Tempo: ${formatPace(avgPaceMs)} min/km\n`;
report += `- Przewyższenia: +${Math.round(totalElevationGain)}m / Max Wysokość: ${Math.round(maxElevation)}m\n`;
if (avgHr > 0) {
report += `- Tętno: Średnie ${avgHr} bpm / Max ${maxHr} bpm\n`;
}
if (avgCadence > 0) {
report += `- Kadencja: ${avgCadence} spm\n`;
}
if (splits.length > 0) {
report += `\nANALIZA MIĘDZYCZASÓW (SPLITS - CO 1 KM):\n`;
splits.forEach(s => {
const pace = formatPace(s.timeMs); // time for 1km = pace
const hrStr = s.avgHr > 0 ? `, HR: ${s.avgHr} bpm` : '';
const elevStr = s.elevGain > 5 ? `, Elev: +${s.elevGain}m` : ''; // show only significant gain
report += `Km ${s.km}: ${pace} min/km${hrStr}${elevStr}\n`;
});
} else {
report += `\n(Brak pełnych kilometrów do analizy międzyczasów)\n`;
}
// Zakończenie
report += `\nWSKAZÓWKI DLA AI: Wykorzystaj te dane do opisu walki na trasie. Zauważ momenty kryzysu (zwolnienie tempa, wysokie tętno) oraz momenty "puszczenia" (zbiegi, przyspieszenie).`;
resolve({
distance: `${totalDistance.toFixed(2)} km`,
duration: msToTime(totalTime),
elevation: `${Math.round(elevationGain)}m`,
stats: uiStats,
richSummary: report
});
} catch (error) {
@@ -68,12 +200,13 @@ export const parseGpxFile = async (file: File): Promise<ActivityStats> => {
}
};
reader.onerror = () => reject(new Error('Error reading file'));
reader.onerror = () => reject(new Error('Błąd odczytu pliku'));
reader.readAsText(file);
});
};
// Haversine formula
// --- POMOCNICZE FUNKCJE MATEMATYCZNE ---
function getDistanceFromLatLonInKm(lat1: number, lon1: number, lat2: number, lon2: number) {
const R = 6371; // Radius of the earth in km
const dLat = deg2rad(lat2 - lat1);
@@ -83,7 +216,7 @@ function getDistanceFromLatLonInKm(lat1: number, lon1: number, lat2: number, lon
Math.cos(deg2rad(lat1)) * Math.cos(deg2rad(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
const d = R * c; // Distance in km
const d = R * c;
return d;
}
@@ -95,6 +228,16 @@ function msToTime(duration: number) {
if (duration <= 0) return "0h 00m";
const minutes = Math.floor((duration / (1000 * 60)) % 60);
const hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
// Jeśli mniej niż godzina, pokaż minuty i sekundy (opcjonalnie, tu zostawiamy format h m)
if (hours === 0) return `${minutes}m`;
return `${hours}h ${minutes}m`;
}
}
function formatPace(msPerKm: number): string {
if (msPerKm <= 0 || !isFinite(msPerKm)) return "-:--";
const totalSeconds = Math.floor(msPerKm / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}