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
This commit is contained in:
Generated
+13
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DeviceTable">
|
||||
<option name="columnSorters">
|
||||
<list>
|
||||
<ColumnSorterState>
|
||||
<option name="column" value="Name" />
|
||||
<option name="order" value="ASCENDING" />
|
||||
</ColumnSorterState>
|
||||
</list>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="previewPanelProviderInfo">
|
||||
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"expo": {
|
||||
"name": "Bincio",
|
||||
"slug": "bincio",
|
||||
"name": "Bincio Autarchive",
|
||||
"slug": "bincio-autarchive",
|
||||
"version": "0.1.0",
|
||||
"orientation": "portrait",
|
||||
"scheme": "bincio",
|
||||
|
||||
+54
-9
@@ -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 (
|
||||
<View style={styles.chartContainer}>
|
||||
{/* Tab row */}
|
||||
@@ -378,15 +383,15 @@ function MetricCharts({ timeseries, loading, accent }: { timeseries: Timeseries
|
||||
))}
|
||||
</View>
|
||||
{/* Chart */}
|
||||
<MetricChart key={tab} times={timeseries.t} values={raw} color={color} unit={unit} decimals={decimals} />
|
||||
<MetricChart key={tab} distances={distances} values={raw} color={color} unit={unit} decimals={decimals} />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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({
|
||||
<Path d={areaPath} fill={`url(#${gradId})`} />
|
||||
<Path d={linePath} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" />
|
||||
</Svg>
|
||||
<Text style={[styles.chartLabel, { color: '#3f3f46', marginBottom: 10 }]}>{fmt(minV)} {unit}</Text>
|
||||
<Text style={[styles.chartLabel, { color: '#3f3f46', marginBottom: 10 }]}>
|
||||
{fmt(minV)} {unit} • {fmtDist(maxD)} km
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
// ── 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][] = [];
|
||||
|
||||
Reference in New Issue
Block a user