Skip to content

Commit

Permalink
Tracking run
Browse files Browse the repository at this point in the history
  • Loading branch information
vedmant committed Mar 9, 2024
1 parent a0e1143 commit cbfc72f
Show file tree
Hide file tree
Showing 9 changed files with 104 additions and 32 deletions.
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
"react-native": "0.73.4",
"react-native-chart-kit": "^6.12.0",
"react-native-gesture-handler": "~2.14.0",
"react-native-maps": "1.10.0",
"react-native-modal-datetime-picker": "^17.1.0",
"react-native-pager-view": "6.2.3",
"react-native-root-toast": "^3.5.1",
Expand Down
4 changes: 2 additions & 2 deletions src/components/Button.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React, { cloneElement } from 'react'
import { ActivityIndicator, Pressable, Text, View } from 'react-native'

export default function ({ label, loading, icon, style, pressableStyle, ...props }) {
export default function ({ label, loading, icon, style, pressableStyle, textStyle, ...props }) {
const iconProps = { weight: 'bold', size: 18, color: 'white', style: { marginRight: 6 } }

return (
Expand All @@ -15,7 +15,7 @@ export default function ({ label, loading, icon, style, pressableStyle, ...props
? <ActivityIndicator {...iconProps} />
: icon && cloneElement(icon, { ...iconProps, ...icon?.props })
}
<Text className="text-white">{label}</Text>
<Text className="text-white" style={textStyle}>{label}</Text>
</Pressable>
</View>
)
Expand Down
29 changes: 29 additions & 0 deletions src/components/MapPath.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import MapView, { Polyline } from 'react-native-maps'
import { View } from 'react-native'
import colors from 'tailwindcss/colors'

export default function ({ locations, ...props }) {
if (!locations.length) { return null }
const lastCoords = locations[locations.length - 1].coords

return (
<View {...props}>
<MapView style={{ width: '100%', height: '100%' }}
scrollEnabled={false}
region={{
latitude: lastCoords.latitude,
longitude: lastCoords.longitude,
latitudeDelta: 0.003,
longitudeDelta: 0.003,
}}
>
<Polyline
coordinates={locations.map(l => l.coords)}
strokeColor="#000" // fallback for when `strokeColors` is not supported by the map-provider
strokeColors={[colors.fuchsia[500]]}
strokeWidth={6}
/>
</MapView>
</View>
)
}
1 change: 1 addition & 0 deletions src/composables/useApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export default async function (url, options = {}) {
const err = new Error(`${resp.status} ${resp.statusText}`)
err.response = resp
err.response.data = await resp.json()
console.error(err.response.data)
throw err
}

Expand Down
62 changes: 47 additions & 15 deletions src/screens/Dashboard/DashboardScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import Button from '@/components/Button'
import { useBackgroundPermissions, useForegroundPermissions } from 'expo-location'
import { useLocationData, useLocationTracking } from '@/services/location'
import { Play, Stop } from 'phosphor-react-native'
import MapPath from '@/components/MapPath'
import dayjs from 'dayjs'
import { useEntriesStore } from '@/stores/entries'
import Toast from 'react-native-root-toast'

function useAskPermissions () {
const [foregroundPermission, askForegroundPermission] = useForegroundPermissions()
Expand All @@ -21,15 +25,24 @@ function useAskPermissions () {
}
}

function formatDuration (seconds) {
const date = new Date(0)
date.setSeconds(seconds)
return date.toISOString().substring(11, 19)
}


export default function () {
const [loading, setLoading] = useState(false)
const [booted, setBooted] = useState(false)
const dashboard = useGeneralStore(s => s.dashboard)
const loadDashboard = useGeneralStore(s => s.loadDashboard)
const isDark = useColorScheme().colorScheme === 'dark'
const askPermissions = useAskPermissions()
const { locations, distance, speed, elevation, descend } = useLocationData()
const { locations, summary, speed } = useLocationData()
const tracking = useLocationTracking()
const [start, setStart] = useState(dayjs())


const dispatchLoadDashboard = async () => {
setLoading(true)
Expand All @@ -50,11 +63,24 @@ export default function () {
await askPermissions()
await tracking.clearTracking()
await tracking.startTracking()
setStart(dayjs())
}

const stopRun = async () => {
await tracking.stopTracking()
const form = {
date: dayjs().format('MM/DD/YYYY'),
distance: summary.distance / 1000,
time: formatDuration(dayjs().diff(start, 'second')),
}

await tracking.clearTracking()

if (summary.distance > 100) {
await useEntriesStore.getState().storeEntry(form)
Toast.show('Your run was saved')
} else {
Toast.show('Run minimum 10 meters to save the record')
}
}

return (
Expand All @@ -66,29 +92,35 @@ export default function () {
<RefreshControl onRefresh={onRefresh} refreshing={loading} tintColor={isDark ? colors.gray[200] : colors.gray[700]} />
}>

<View className="pb-2 pt-2">
<View className="pb-2 pt-2 items-center">
{tracking.isTracking
? <>
{distance === 0
? <Text className="dark:text-white">You didn't walk yet, start the location tracking and start walking.</Text>
: <Text className="dark:text-white">You run {distance} meters. Speed: {speed} km/h. Elevation: {elevation} m. Descend {descend} m.</Text>
}
<Text className="dark:text-white">
Time: {formatDuration(dayjs().diff(start, 'second'))}{'\n'}
Distance: {Math.round(summary.distance)} m{'\n'}
Speed: {speed} km/h{'\n'}
Elevation: {Math.round(summary.elevation)} m{'\n'}
Descend: {Math.round(summary.descend)} m
</Text>
<Button label="Stop" icon={<Stop weight="fill" />} className="mt-2" onPress={stopRun} />
</>
: <Button label="Track My Run"
className="rounded-full bg-amber-700"
: <Button label="Start"
className="rounded-full bg-fuchsia-700 w-24 py21 shadow-lg shadow-gray-700 py-1"
pressableStyle={{ flexDirection: 'column' }}
icon={<Play size={40} weight="fill" className="mb-2" />}
textStyle={{ fontWeight: 'bold' }}
icon={<Play size={40} weight="fill" className="mb-1" />}
onPress={trackRun}
/>
}
</View>

{locations.slice(-5).map((item) => (
<View className="flex flex-row gap-2" key={item.timestamp}>
<Text className="dark:text-white">{JSON.stringify(item.coords)}</Text>
</View>
))}
<MapPath locations={locations} className="h-64" />

{/*{locations.slice(-5).map((item) => (*/}
{/* <View className="flex flex-row gap-2" key={item.timestamp}>*/}
{/* <Text className="dark:text-white">{JSON.stringify(item.coords)}</Text>*/}
{/* </View>*/}
{/*))}*/}

<Panel header="This week">
<Text className="dark:text-white">
Expand Down
2 changes: 1 addition & 1 deletion src/screens/Entries/EntriesScreen.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export default function () {
<Text className="dark:text-white">
Date: {dayjs(item.date).format('MM/DD/YY')}
</Text>
<Text className="dark:text-white">Distance: {item.distance} km</Text>
<Text className="dark:text-white">Distance: {Math.round(item.distance * 100) / 100} km</Text>
<Text className="dark:text-white">Time: {item.time}</Text>
</View>
<View style={{ flex: 1 }}>
Expand Down
34 changes: 22 additions & 12 deletions src/services/location/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,54 +23,64 @@ export function useLocationTracking() {
setIsTracking(false)
},
clearTracking: async () => {
console.log('clearTracking', isTracking)
if (isTracking) {
await Track.stopTracking()
console.log('await Track.stopTracking()')
setIsTracking(false)
}
console.log('await Storage.clearLocations()')
await Storage.clearLocations()
},
}
}

const baseSummary = { distance: 0, elevation: 0, descend: 0 }

export function useLocationData(interval = 1000) {
const [locations, setLocations] = useState<LocationObject[]>([])
const [distance, setDistance] = useState(0)
const [elevation, setElevation] = useState(0)
const [descend, setDescend] = useState(0)
const [summary, setSummary] = useState(baseSummary)

useEffect(() => {
const update = async () => {
console.log('update')
const storedLocations = await Storage.getLocations()

// Clear locations
if (!storedLocations.length && locations.length) {
setLocations([])
setSummary(baseSummary)
return
}

// No new locations added
if (storedLocations.length <= locations.length) {
return
}

setLocations(storedLocations)

// Update summary data
if (locations.length > 1) {
const prev = locations[locations.length - 2].coords
const next = locations[locations.length - 1].coords
setDistance(distance + getDistance(prev, next, 0.01))
if (next.altitude > prev.altitude) {
setElevation(elevation + next.altitude - prev.altitude)
} else {
setDescend(descend - next.altitude - prev.altitude)
}
setSummary({
distance: summary.distance + getDistance(prev, next, 0.01),
elevation: summary.elevation + Math.min(0, next.altitude - prev.altitude),
descend: summary.descend + Math.abs(Math.max(0, next.altitude - prev.altitude)),
})
}
}
update()
console.log('setInterval')
const timerId = window.setInterval(update, interval)

return () => window.clearInterval(timerId)
}, [locations])
}, [locations, summary])

const speed = useMemo(() => {
return Math.round((locations[locations.length - 1]?.coords?.speed || 0) * 3.6)
}, [locations])

return { locations, distance, speed, elevation, descend }
return { locations, summary, speed }
}

3 changes: 1 addition & 2 deletions src/services/location/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,12 @@ export async function stopTracking() {
* This method isn't "directly" connected to React, that's why we store the data locally.
*/
TaskManager.defineTask(LOCATION_TASK_NAME, async (event) => {
console.log(event)
if (event.error) {
return console.error('[tracking]', 'Something went wrong within the background location task...', event.error)
}

const locations = (event.data as any).locations as Location.LocationObject[]
console.log('[tracking]', 'Received new locations', locations)
// console.log('[tracking]', 'Received new locations', locations)

try {
// have to add it sequentially, parses/serializes existing JSON
Expand Down

0 comments on commit cbfc72f

Please sign in to comment.