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">
|
<main class="container mx-auto p-4">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h2 class="text-xl mb-4">Your Cycling Tracks</h2>
|
<h2 class="text-xl mb-4">Your Cycling Tracks</h2>
|
||||||
<MapView />
|
<MapView :tracks="tracks" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<FileUpload @files-uploaded="handleFilesUploaded" />
|
<FileUpload @files-uploaded="handleFilesUploaded" />
|
||||||
</div>
|
</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>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -3,13 +3,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref, onMounted, onUnmounted } from 'vue'
|
import { ref, onMounted, onUnmounted, watch } from 'vue'
|
||||||
import L from 'leaflet'
|
import L from 'leaflet'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'MapView',
|
name: 'MapView',
|
||||||
setup() {
|
props: {
|
||||||
|
tracks: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup(props) {
|
||||||
const map = ref(null)
|
const map = ref(null)
|
||||||
|
const trackLayers = ref([])
|
||||||
|
|
||||||
const getLocationFromIP = async () => {
|
const getLocationFromIP = async () => {
|
||||||
try {
|
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 initMap = async () => {
|
||||||
const coords = await getLocationFromIP()
|
const coords = await getLocationFromIP()
|
||||||
|
|
||||||
@@ -31,8 +93,14 @@ export default {
|
|||||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
}).addTo(map.value)
|
}).addTo(map.value)
|
||||||
|
|
||||||
|
// Initial track rendering
|
||||||
|
renderTracks()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Watch for track changes
|
||||||
|
watch(() => props.tracks, renderTracks, { deep: true })
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initMap()
|
initMap()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user