Implemented GPX file parsing with track statistics

- Using gpx-parser-builder for parsing GPX files
- Calculating some preliminary statistics
- Some error handling
This commit is contained in:
Atharva Sawant
2024-11-02 19:23:16 +05:30
parent 5d9648e1ed
commit be5ff4992d
4 changed files with 273 additions and 5 deletions

3
.gitignore vendored
View File

@@ -22,3 +22,6 @@ dist-ssr
*.njsproj
*.sln
*.sw?
# Data used for testing
test-files

View File

@@ -19,8 +19,10 @@
</template>
<script>
import { ref } from 'vue'
import MapView from './components/MapView.vue'
import FileUpload from './components/FileUpload.vue'
import { useTracks } from './composables/useTracks.js'
export default {
name: 'App',
@@ -28,10 +30,28 @@ export default {
MapView,
FileUpload
},
methods: {
handleFilesUploaded(files) {
console.log('Files uploaded:', files)
// TODO: Parse GPX files and display on map
setup() {
const { tracks, addTracks, totalStats } = useTracks()
const isLoading = ref(false)
const handleFilesUploaded = async (files) => {
isLoading.value = true
try {
const newTracks = await addTracks(files)
console.log('Parsed tracks:', newTracks)
console.log('Total stats:', totalStats.value)
} catch (error) {
console.error('Error processing files:', error)
} finally {
isLoading.value = false
}
}
return {
tracks,
totalStats,
isLoading,
handleFilesUploaded
}
}
}

View File

@@ -0,0 +1,71 @@
import { ref, computed } from 'vue'
import { parseGPXFile } from '../utils/gpxParser.js'
const tracks = ref([])
export const useTracks = () => {
const addTracks = async (files) => {
const newTracks = []
for (const file of files) {
try {
const parsedTracks = await parseGPXFile(file)
newTracks.push(...parsedTracks)
} catch (error) {
console.error(`Error parsing ${file.name}:`, error)
}
}
tracks.value.push(...newTracks)
return newTracks
}
const removeTracks = (trackIndices) => {
trackIndices.sort((a, b) => b - a) // Sort descending to remove from end first
trackIndices.forEach(index => {
tracks.value.splice(index, 1)
})
}
const clearAllTracks = () => {
tracks.value = []
}
const totalStats = computed(() => {
if (tracks.value.length === 0) {
return {
totalTracks: 0,
totalDistance: 0,
totalDuration: 0,
totalElevationGain: 0,
avgSpeed: 0
}
}
const totals = tracks.value.reduce((acc, track) => {
if (track.stats) {
acc.distance += track.stats.distance || 0
acc.duration += track.stats.duration || 0
acc.elevationGain += track.stats.elevationGain || 0
}
return acc
}, { distance: 0, duration: 0, elevationGain: 0 })
return {
totalTracks: tracks.value.length,
totalDistance: Math.round(totals.distance / 1000 * 10) / 10, // km
totalDuration: Math.round(totals.duration / 3600 * 10) / 10, // hours
totalElevationGain: Math.round(totals.elevationGain), // meters
avgSpeed: totals.duration > 0 ?
Math.round((totals.distance / totals.duration) * 3.6 * 10) / 10 : 0 // km/h
}
})
return {
tracks: tracks.value,
addTracks,
removeTracks,
clearAllTracks,
totalStats
}
}

174
src/utils/gpxParser.js Normal file
View File

@@ -0,0 +1,174 @@
import gpxParserBuilder from 'gpx-parser-builder'
export const parseGPXFile = async (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.onload = (e) => {
try {
const gpxContent = e.target.result
const gpx = gpxParserBuilder.parse(gpxContent)
console.log('Parsed GPX structure:', gpx)
// Extract track data - handle both tracks and routes
let tracks = []
// Handle tracks (trk property)
if (gpx.trk && Array.isArray(gpx.trk)) {
tracks = gpx.trk.map(track => {
const segments = track.trkseg?.map(segment => {
const points = segment.trkpt?.map(point => ({
lat: parseFloat(point.$.lat),
lng: parseFloat(point.$.lon),
elevation: point.ele ? parseFloat(point.ele[0]) : null,
time: point.time ? point.time[0] : null
})) || []
return { points }
}) || []
return {
name: track.name?.[0] || file.name,
segments,
metadata: {
fileName: file.name,
fileSize: file.size,
uploadDate: new Date().toISOString()
}
}
})
}
// Handle routes if no tracks (rte property)
if (tracks.length === 0 && gpx.rte && Array.isArray(gpx.rte)) {
tracks = gpx.rte.map(route => {
const points = route.rtept?.map(point => ({
lat: parseFloat(point.$.lat),
lng: parseFloat(point.$.lon),
elevation: point.ele ? parseFloat(point.ele[0]) : null,
time: point.time ? point.time[0] : null
})) || []
return {
name: route.name?.[0] || file.name,
segments: [{ points }],
metadata: {
fileName: file.name,
fileSize: file.size,
uploadDate: new Date().toISOString()
}
}
})
}
// Handle waypoints if no tracks or routes (wpt property)
if (tracks.length === 0 && gpx.wpt && Array.isArray(gpx.wpt)) {
const points = gpx.wpt.map(waypoint => ({
lat: parseFloat(waypoint.$.lat),
lng: parseFloat(waypoint.$.lon),
elevation: waypoint.ele ? parseFloat(waypoint.ele[0]) : null,
time: waypoint.time ? waypoint.time[0] : null
}))
tracks = [{
name: gpx.metadata?.name?.[0] || file.name,
segments: [{ points }],
metadata: {
fileName: file.name,
fileSize: file.size,
uploadDate: new Date().toISOString()
}
}]
}
// Calculate track statistics
const processedTracks = tracks.map(track => {
const stats = calculateTrackStats(track)
return { ...track, stats }
})
resolve(processedTracks)
} catch (error) {
reject(new Error(`Failed to parse GPX file: ${error.message}`))
}
}
reader.onerror = () => {
reject(new Error('Failed to read file'))
}
reader.readAsText(file)
})
}
const calculateTrackStats = (track) => {
if (!track.segments || track.segments.length === 0) {
return {
distance: 0,
duration: 0,
avgSpeed: 0,
elevationGain: 0,
startTime: null,
endTime: null
}
}
let totalDistance = 0
let totalElevationGain = 0
let startTime = null
let endTime = null
let prevPoint = null
track.segments.forEach(segment => {
segment.points.forEach(point => {
// Track time bounds
if (point.time) {
const pointTime = new Date(point.time)
if (!startTime || pointTime < startTime) startTime = pointTime
if (!endTime || pointTime > endTime) endTime = pointTime
}
// Calculate distance and elevation
if (prevPoint) {
totalDistance += calculateDistance(prevPoint, point)
if (point.elevation && prevPoint.elevation) {
const elevDiff = point.elevation - prevPoint.elevation
if (elevDiff > 0) totalElevationGain += elevDiff
}
}
prevPoint = point
})
})
const duration = startTime && endTime ?
(endTime.getTime() - startTime.getTime()) / 1000 : 0 // seconds
const avgSpeed = duration > 0 ? (totalDistance / duration) * 3.6 : 0 // km/h
return {
distance: Math.round(totalDistance), // meters
duration: Math.round(duration), // seconds
avgSpeed: Math.round(avgSpeed * 10) / 10, // km/h
elevationGain: Math.round(totalElevationGain), // meters
startTime: startTime ? startTime.toISOString() : null,
endTime: endTime ? endTime.toISOString() : null
}
}
// Haversine formula for distance between two points
const calculateDistance = (point1, point2) => {
const R = 6371e3 // Earth's radius in meters
const φ1 = point1.lat * Math.PI / 180
const φ2 = point2.lat * Math.PI / 180
const Δφ = (point2.lat - point1.lat) * Math.PI / 180
const Δλ = (point2.lng - point1.lng) * Math.PI / 180
const a = Math.sin(Δφ/2) * Math.sin(Δφ/2) +
Math.cos(φ1) * Math.cos(φ2) *
Math.sin(Δλ/2) * Math.sin(Δλ/2)
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a))
return R * c
}