UNPKG

expo-running-kit

Version:

Expo native module for tracking running and walking workouts — GPS, pace, cadence, auto-pause, and laps.

148 lines 5.75 kB
import { useEffect, useRef, useState } from "react"; import { useEvent } from "expo"; import RunningKit from "../RunningKitModule"; // --- Conversion helpers --- const M_TO_KM = 0.001; const M_TO_MI = 0.000621371; export function toDisplayDistance(meters, units) { return units === "metric" ? meters * M_TO_KM : meters * M_TO_MI; } export function formatPace(ms, units) { if (ms <= 0) return null; const secPer = units === "metric" ? 1000 / ms : 1609.34 / ms; const min = Math.floor(secPer / 60); const sec = Math.floor(secPer % 60); return `${min}:${sec.toString().padStart(2, "0")}`; } export function getGpsQuality(accuracy) { if (accuracy < 10) return "excellent"; if (accuracy < 25) return "good"; if (accuracy < 50) return "fair"; return "poor"; } // Haversine formula — accurate distance between two GPS coordinates function haversineDistance(lat1, lon1, lat2, lon2) { const R = 6371000; // Earth radius in meters const toRad = (deg) => (deg * Math.PI) / 180; const dLat = toRad(lat2 - lat1); const dLon = toRad(lon2 - lon1); const a = Math.sin(dLat / 2) ** 2 + Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) * Math.sin(dLon / 2) ** 2; return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); } const MIN_PACE_SPEED = 0.5; // m/s — below this speed pace is meaningless export function useLocation({ units, speedSmoothingWindow, durationRef, }) { const [gpsQuality, setGpsQuality] = useState("poor"); const [distanceMeters, setDistanceMeters] = useState(0); const [speedStats, setSpeedStats] = useState({ current: 0, avg: 0, max: 0, }); const [paceStats, setPaceStats] = useState({ current: null, avg: null, best: null, }); const [smoothedSpeedMs, setSmoothedSpeedMs] = useState(0); const [steps, setSteps] = useState({ total: 0, cadence: 0 }); // Refs for mutable accumulation — don't need re-renders const distanceRef = useRef(0); const lastLatRef = useRef(null); const lastLonRef = useRef(null); const speedWindowRef = useRef([]); const maxSpeedRef = useRef(0); const bestPaceMsRef = useRef(0); const gpsTimeoutRef = useRef(null); const locationPayload = useEvent(RunningKit, "onLocationUpdate"); const stepPayload = useEvent(RunningKit, "onStepUpdate"); useEffect(() => { if (!stepPayload) return; setSteps({ total: stepPayload.steps, cadence: stepPayload.cadence }); }, [stepPayload]); useEffect(() => { if (!locationPayload) return; const { latitude, longitude, speed: rawSpeed, accuracy } = locationPayload; // GPS quality — reset to "poor" after 4s with no updates (GPS lost / workout stopped) if (gpsTimeoutRef.current) clearTimeout(gpsTimeoutRef.current); gpsTimeoutRef.current = setTimeout(() => setGpsQuality("poor"), 4000); setGpsQuality(getGpsQuality(accuracy)); // Distance — only accumulate if user moved > 1m to filter GPS drift noise if (lastLatRef.current !== null && lastLonRef.current !== null) { const delta = haversineDistance(lastLatRef.current, lastLonRef.current, latitude, longitude); if (delta > 1) { distanceRef.current += delta; setDistanceMeters(distanceRef.current); } } lastLatRef.current = latitude; lastLonRef.current = longitude; // Speed smoothing — rolling average const window = speedWindowRef.current; window.push(rawSpeed); if (window.length > speedSmoothingWindow) window.shift(); const smoothedMs = window.reduce((a, b) => a + b, 0) / window.length; setSmoothedSpeedMs(smoothedMs); // Max speed if (smoothedMs > maxSpeedRef.current) maxSpeedRef.current = smoothedMs; // Avg speed — use ref for duration to avoid stale closure const currentDuration = durationRef.current; const avgMs = currentDuration > 0 ? distanceRef.current / currentDuration : 0; // Best pace — highest speed seen above threshold if (smoothedMs >= MIN_PACE_SPEED && smoothedMs > bestPaceMsRef.current) { bestPaceMsRef.current = smoothedMs; } setSpeedStats({ current: smoothedMs, avg: avgMs, max: maxSpeedRef.current, }); setPaceStats({ current: smoothedMs >= MIN_PACE_SPEED ? formatPace(smoothedMs, units) : null, avg: avgMs > 0 ? formatPace(avgMs, units) : null, best: bestPaceMsRef.current > 0 ? formatPace(bestPaceMsRef.current, units) : null, }); }, [locationPayload]); function resetLocation() { distanceRef.current = 0; lastLatRef.current = null; lastLonRef.current = null; speedWindowRef.current = []; maxSpeedRef.current = 0; bestPaceMsRef.current = 0; if (gpsTimeoutRef.current) { clearTimeout(gpsTimeoutRef.current); gpsTimeoutRef.current = null; } setDistanceMeters(0); setGpsQuality("poor"); setSmoothedSpeedMs(0); setSpeedStats({ current: 0, avg: 0, max: 0 }); setPaceStats({ current: null, avg: null, best: null }); setSteps({ total: 0, cadence: 0 }); } return { gpsQuality, distanceMeters, distanceRef, speedStats, paceStats, smoothedSpeedMs, maxSpeedRef, bestPaceMsRef, steps, resetLocation, }; } //# sourceMappingURL=useLocation.js.map