Added track visualization
- Render GPX tracks on map - Statistics dashboard with total distance, duration, elevation - Reactive updates when tracks are added
This commit is contained in:
29
src/App.vue
29
src/App.vue
@@ -8,12 +8,39 @@
|
||||
<main class="container mx-auto p-4">
|
||||
<div class="mb-6">
|
||||
<h2 class="text-xl mb-4">Your Cycling Tracks</h2>
|
||||
<MapView />
|
||||
<MapView :tracks="tracks" />
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<FileUpload @files-uploaded="handleFilesUploaded" />
|
||||
</div>
|
||||
|
||||
<!-- Track Statistics -->
|
||||
<div v-if="tracks.length > 0" class="mb-6">
|
||||
<h3 class="text-lg font-medium mb-3">Statistics</h3>
|
||||
<div class="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div class="bg-gray-50 p-4 rounded-lg text-center">
|
||||
<div class="text-2xl font-bold text-blue-600">{{ totalStats.totalTracks }}</div>
|
||||
<div class="text-sm text-gray-600">Total Tracks</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg text-center">
|
||||
<div class="text-2xl font-bold text-green-600">{{ totalStats.totalDistance }}</div>
|
||||
<div class="text-sm text-gray-600">Total Distance (km)</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg text-center">
|
||||
<div class="text-2xl font-bold text-purple-600">{{ totalStats.totalDuration }}</div>
|
||||
<div class="text-sm text-gray-600">Total Hours</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg text-center">
|
||||
<div class="text-2xl font-bold text-orange-600">{{ totalStats.avgSpeed }}</div>
|
||||
<div class="text-sm text-gray-600">Avg Speed (km/h)</div>
|
||||
</div>
|
||||
<div class="bg-gray-50 p-4 rounded-lg text-center">
|
||||
<div class="text-2xl font-bold text-red-600">{{ totalStats.totalElevationGain }}</div>
|
||||
<div class="text-sm text-gray-600">Elevation Gain (m)</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -3,13 +3,20 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||
import L from 'leaflet'
|
||||
|
||||
export default {
|
||||
name: 'MapView',
|
||||
setup() {
|
||||
props: {
|
||||
tracks: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
const map = ref(null)
|
||||
const trackLayers = ref([])
|
||||
|
||||
const getLocationFromIP = async () => {
|
||||
try {
|
||||
@@ -23,6 +30,61 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
const getTrackColor = (index) => {
|
||||
const colors = [
|
||||
'#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FFEAA7',
|
||||
'#DDA0DD', '#98D8C8', '#F7DC6F', '#BB8FCE', '#85C1E9'
|
||||
]
|
||||
return colors[index % colors.length]
|
||||
}
|
||||
|
||||
const renderTracks = () => {
|
||||
// Clear existing track layers
|
||||
trackLayers.value.forEach(layer => {
|
||||
map.value.removeLayer(layer)
|
||||
})
|
||||
trackLayers.value = []
|
||||
|
||||
if (!props.tracks || props.tracks.length === 0) return
|
||||
|
||||
const allBounds = []
|
||||
|
||||
props.tracks.forEach((track, trackIndex) => {
|
||||
const color = getTrackColor(trackIndex)
|
||||
|
||||
track.segments.forEach(segment => {
|
||||
if (segment.points && segment.points.length > 0) {
|
||||
const latLngs = segment.points.map(point => [point.lat, point.lng])
|
||||
|
||||
const polyline = L.polyline(latLngs, {
|
||||
color: color,
|
||||
weight: 3,
|
||||
opacity: 0.8
|
||||
}).bindPopup(`
|
||||
<div class="font-medium">${track.name}</div>
|
||||
<div class="text-sm text-gray-600 mt-1">
|
||||
Distance: ${(track.stats?.distance / 1000 || 0).toFixed(1)} km<br>
|
||||
Duration: ${Math.floor((track.stats?.duration || 0) / 3600)}:${Math.floor(((track.stats?.duration || 0) % 3600) / 60).toString().padStart(2, '0')}<br>
|
||||
Avg Speed: ${track.stats?.avgSpeed || 0} km/h
|
||||
</div>
|
||||
`)
|
||||
|
||||
polyline.addTo(map.value)
|
||||
trackLayers.value.push(polyline)
|
||||
|
||||
// Add bounds for this segment
|
||||
allBounds.push(...latLngs)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// Fit map to show all tracks
|
||||
if (allBounds.length > 0) {
|
||||
const group = new L.featureGroup(trackLayers.value)
|
||||
map.value.fitBounds(group.getBounds(), { padding: [20, 20] })
|
||||
}
|
||||
}
|
||||
|
||||
const initMap = async () => {
|
||||
const coords = await getLocationFromIP()
|
||||
|
||||
@@ -31,8 +93,14 @@ export default {
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(map.value)
|
||||
|
||||
// Initial track rendering
|
||||
renderTracks()
|
||||
}
|
||||
|
||||
// Watch for track changes
|
||||
watch(() => props.tracks, renderTracks, { deep: true })
|
||||
|
||||
onMounted(() => {
|
||||
initMap()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user