Naprawa obsługi GPX, czyszczenie projektu, naprawa błędów związanych z obrazami
This commit is contained in:
@@ -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')}`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user