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": {
|
"expo": {
|
||||||
"name": "Bincio",
|
"name": "Bincio Autarchive",
|
||||||
"slug": "bincio",
|
"slug": "bincio-autarchive",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"orientation": "portrait",
|
"orientation": "portrait",
|
||||||
"scheme": "bincio",
|
"scheme": "bincio",
|
||||||
|
|||||||
+54
-9
@@ -21,6 +21,7 @@ type Timeseries = {
|
|||||||
power_w?: (number | null)[] | null;
|
power_w?: (number | null)[] | null;
|
||||||
lat?: (number | null)[] | null;
|
lat?: (number | null)[] | null;
|
||||||
lon?: (number | null)[] | null;
|
lon?: (number | null)[] | null;
|
||||||
|
distance_m?: number[] | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
// ── Screen ───────────────────────────────────────────────────────────────────
|
// ── Screen ───────────────────────────────────────────────────────────────────
|
||||||
@@ -361,6 +362,10 @@ function MetricCharts({ timeseries, loading, accent }: { timeseries: Timeseries
|
|||||||
const { color, unit, decimals } = TAB_META[tab];
|
const { color, unit, decimals } = TAB_META[tab];
|
||||||
const raw = seriesMap[tab]!;
|
const raw = seriesMap[tab]!;
|
||||||
|
|
||||||
|
const distances = timeseries.distance_m
|
||||||
|
? timeseries.distance_m
|
||||||
|
: calculateDistanceFromCoords(timeseries.lat, timeseries.lon);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.chartContainer}>
|
<View style={styles.chartContainer}>
|
||||||
{/* Tab row */}
|
{/* Tab row */}
|
||||||
@@ -378,15 +383,15 @@ function MetricCharts({ timeseries, loading, accent }: { timeseries: Timeseries
|
|||||||
))}
|
))}
|
||||||
</View>
|
</View>
|
||||||
{/* Chart */}
|
{/* 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>
|
</View>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function MetricChart({
|
function MetricChart({
|
||||||
times, values, color, unit, decimals,
|
distances, values, color, unit, decimals,
|
||||||
}: {
|
}: {
|
||||||
times: number[];
|
distances: number[] | null;
|
||||||
values: (number | null)[];
|
values: (number | null)[];
|
||||||
color: string;
|
color: string;
|
||||||
unit: string;
|
unit: string;
|
||||||
@@ -396,25 +401,28 @@ function MetricChart({
|
|||||||
const H = 100;
|
const H = 100;
|
||||||
const PAD = 4;
|
const PAD = 4;
|
||||||
|
|
||||||
|
if (!distances) return null;
|
||||||
|
|
||||||
// Downsample to ≤300 points
|
// Downsample to ≤300 points
|
||||||
const step = Math.max(1, Math.floor(values.length / 300));
|
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 vs = values.filter((_, i) => i % step === 0).map(v => v ?? 0);
|
||||||
|
|
||||||
const minV = Math.min(...vs);
|
const minV = Math.min(...vs);
|
||||||
const maxV = Math.max(...vs);
|
const maxV = Math.max(...vs);
|
||||||
const range = maxV - minV || 1;
|
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 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 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 gradId = `grad-${color.replace('#', '')}`;
|
||||||
|
|
||||||
const fmt = (v: number) => decimals === 0 ? String(Math.round(v)) : v.toFixed(decimals);
|
const fmt = (v: number) => decimals === 0 ? String(Math.round(v)) : v.toFixed(decimals);
|
||||||
|
const fmtDist = (d: number) => (d / 1000).toFixed(1);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -429,13 +437,50 @@ function MetricChart({
|
|||||||
<Path d={areaPath} fill={`url(#${gradId})`} />
|
<Path d={areaPath} fill={`url(#${gradId})`} />
|
||||||
<Path d={linePath} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" />
|
<Path d={linePath} fill="none" stroke={color} strokeWidth="1.5" strokeLinejoin="round" />
|
||||||
</Svg>
|
</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 ───────────────────────────────────────────────────────────────────
|
// ── 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
|
// Returns [west, south, east, north] per LngLatBounds spec
|
||||||
function geoJsonBounds(gj: object): [number, number, number, number] | null {
|
function geoJsonBounds(gj: object): [number, number, number, number] | null {
|
||||||
const coords: [number, number][] = [];
|
const coords: [number, number][] = [];
|
||||||
|
|||||||
Reference in New Issue
Block a user