From 3522568ac373148ec459ee79b1af8f8f82c094ed Mon Sep 17 00:00:00 2001 From: Davide Scaini Date: Wed, 3 Jun 2026 00:08:45 +0200 Subject: [PATCH] feat: elevation profile with distance axis, rename app, add ABI splits - Show distance (not time) on X-axis for elevation and speed charts - Rename app from "Bincio" to "Bincio Autarchive" - Reinstate arm64-v8a and armeabi-v7a ABI splits for separate APK builds - Add Haversine distance calculation from GPS coordinates --- .idea/deviceManager.xml | 13 +++++++++ .idea/markdown.xml | 8 ++++++ app.json | 4 +-- app/activity/[id].tsx | 63 +++++++++++++++++++++++++++++++++++------ 4 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 .idea/deviceManager.xml create mode 100644 .idea/markdown.xml diff --git a/.idea/deviceManager.xml b/.idea/deviceManager.xml new file mode 100644 index 0000000..91f9558 --- /dev/null +++ b/.idea/deviceManager.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/.idea/markdown.xml b/.idea/markdown.xml new file mode 100644 index 0000000..c61ea33 --- /dev/null +++ b/.idea/markdown.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app.json b/app.json index 90255e3..5a8dfa8 100644 --- a/app.json +++ b/app.json @@ -1,7 +1,7 @@ { "expo": { - "name": "Bincio", - "slug": "bincio", + "name": "Bincio Autarchive", + "slug": "bincio-autarchive", "version": "0.1.0", "orientation": "portrait", "scheme": "bincio", diff --git a/app/activity/[id].tsx b/app/activity/[id].tsx index 7b3aeb9..c7ebdd6 100644 --- a/app/activity/[id].tsx +++ b/app/activity/[id].tsx @@ -21,6 +21,7 @@ type Timeseries = { power_w?: (number | null)[] | null; lat?: (number | null)[] | null; lon?: (number | null)[] | null; + distance_m?: number[] | null; }; // ── Screen ─────────────────────────────────────────────────────────────────── @@ -361,6 +362,10 @@ function MetricCharts({ timeseries, loading, accent }: { timeseries: Timeseries const { color, unit, decimals } = TAB_META[tab]; const raw = seriesMap[tab]!; + const distances = timeseries.distance_m + ? timeseries.distance_m + : calculateDistanceFromCoords(timeseries.lat, timeseries.lon); + return ( {/* Tab row */} @@ -378,15 +383,15 @@ function MetricCharts({ timeseries, loading, accent }: { timeseries: Timeseries ))} {/* Chart */} - + ); } function MetricChart({ - times, values, color, unit, decimals, + distances, values, color, unit, decimals, }: { - times: number[]; + distances: number[] | null; values: (number | null)[]; color: string; unit: string; @@ -396,25 +401,28 @@ function MetricChart({ const H = 100; const PAD = 4; + if (!distances) return null; + // Downsample to ≤300 points const step = Math.max(1, Math.floor(values.length / 300)); - const ts = times.filter((_, i) => i % step === 0); + const ds = distances.filter((_, i) => i % step === 0); const vs = values.filter((_, i) => i % step === 0).map(v => v ?? 0); const minV = Math.min(...vs); const maxV = Math.max(...vs); const range = maxV - minV || 1; - const maxT = ts[ts.length - 1] || 1; + const maxD = ds[ds.length - 1] || 1; - const x = (t: number) => PAD + (t / maxT) * (W - PAD * 2); + const x = (d: number) => PAD + (d / maxD) * (W - PAD * 2); const y = (v: number) => PAD + (1 - (v - minV) / range) * (H - PAD * 2); - const pts = ts.map((t, i) => `${x(t).toFixed(1)},${y(vs[i]).toFixed(1)}`); + const pts = ds.map((d, i) => `${x(d).toFixed(1)},${y(vs[i]).toFixed(1)}`); const linePath = `M ${pts.join(' L ')}`; - const areaPath = `M ${x(ts[0])},${H} L ${pts.join(' L ')} L ${x(maxT)},${H} Z`; + const areaPath = `M ${x(ds[0])},${H} L ${pts.join(' L ')} L ${x(maxD)},${H} Z`; const gradId = `grad-${color.replace('#', '')}`; const fmt = (v: number) => decimals === 0 ? String(Math.round(v)) : v.toFixed(decimals); + const fmtDist = (d: number) => (d / 1000).toFixed(1); return ( <> @@ -429,13 +437,50 @@ function MetricChart({ - {fmt(minV)} {unit} + + {fmt(minV)} {unit} • {fmtDist(maxD)} km + ); } // ── Helpers ─────────────────────────────────────────────────────────────────── +function calculateDistanceFromCoords( + lats: (number | null)[] | null | undefined, + lons: (number | null)[] | null | undefined, +): number[] { + if (!lats || !lons) return []; + + const distances: number[] = [0]; + let cumulative = 0; + + for (let i = 1; i < lats.length; i++) { + const lat1 = lats[i - 1]; + const lon1 = lons[i - 1]; + const lat2 = lats[i]; + const lon2 = lons[i]; + + if (lat1 == null || lon1 == null || lat2 == null || lon2 == null) { + distances.push(cumulative); + continue; + } + + // Haversine formula + const R = 6371000; // Earth's radius in meters + const dLat = (lat2 - lat1) * Math.PI / 180; + const dLon = (lon2 - lon1) * Math.PI / 180; + const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + cumulative += R * c; + distances.push(cumulative); + } + + return distances; +} + // Returns [west, south, east, north] per LngLatBounds spec function geoJsonBounds(gj: object): [number, number, number, number] | null { const coords: [number, number][] = [];