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:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -21,4 +21,7 @@ dist-ssr
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.sw?
|
||||
|
||||
# Data used for testing
|
||||
test-files
|
||||
|
||||
28
src/App.vue
28
src/App.vue
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
71
src/composables/useTracks.js
Normal file
71
src/composables/useTracks.js
Normal 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
174
src/utils/gpxParser.js
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user