Files
TrackMap/src/components/MapView.vue
2025-08-27 01:37:28 +05:30

171 lines
5.6 KiB
Vue

<template>
<div id="map" class="w-full h-full rounded-lg shadow-md"></div>
</template>
<script>
import { ref, onMounted, onUnmounted, watch } from 'vue'
import L from 'leaflet'
export default {
name: 'MapView',
props: {
tracks: {
type: Array,
default: () => []
},
selectedTrackIndex: {
type: Number,
default: null
}
},
setup(props) {
const map = ref(null)
const trackLayers = ref([])
const hasAutoFitted = ref(false)
const getLocationFromIP = async () => {
try {
const response = await fetch('https://ipapi.co/json/')
const data = await response.json()
return [data.latitude, data.longitude]
} catch (error) {
console.log('Could not get location from IP:', error)
// Default to San Francisco for localhost/fallback
return [37.7749, -122.4194]
}
}
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 and popups
trackLayers.value.forEach(layer => {
map.value.removeLayer(layer)
})
trackLayers.value = []
// Close any existing popups
map.value.closePopup()
if (!props.tracks || props.tracks.length === 0) return
const allBounds = []
let selectedTrackBounds = []
let selectedTrackPopupOpened = false
props.tracks.forEach((track, trackIndex) => {
const color = getTrackColor(trackIndex)
const isSelected = props.selectedTrackIndex === 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: isSelected ? 8 : 3,
opacity: isSelected ? 1.0 : 0.6,
zIndexOffset: isSelected ? 1000 : 0
}).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, trackIndex })
// Open popup for selected track (only for first segment)
if (isSelected && !selectedTrackPopupOpened) {
// Find the northernmost point (highest latitude)
const northernmostPoint = latLngs.reduce((north, current) =>
current[0] > north[0] ? current : north
)
// Create popup at northernmost point
const popup = L.popup()
.setLatLng(northernmostPoint)
.setContent(`
<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>
`)
.openOn(map.value)
selectedTrackPopupOpened = true
}
// Add bounds for this segment
allBounds.push(...latLngs)
// Collect bounds for selected track
if (isSelected) {
selectedTrackBounds.push(...latLngs)
}
}
})
})
// Focus on selected track if one is selected
if (selectedTrackBounds.length > 0) {
const selectedGroup = L.latLngBounds(selectedTrackBounds)
// Add extra padding at the top to accommodate popup
// Popup is roughly 80-100px tall, so add more top padding
map.value.fitBounds(selectedGroup, {
paddingTopLeft: [50, 120], // Extra top padding for popup
paddingBottomRight: [50, 50]
})
} else if (allBounds.length > 0 && !hasAutoFitted.value) {
// Only auto-fit to all tracks on initial load, not when deselecting
const group = new L.featureGroup(trackLayers.value.map(item => item.polyline))
map.value.fitBounds(group.getBounds(), { padding: [20, 20] })
hasAutoFitted.value = true
}
}
const initMap = async () => {
const coords = await getLocationFromIP()
map.value = L.map('map').setView(coords, 10)
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
}).addTo(map.value)
// Initial track rendering
renderTracks()
}
// Watch for track changes and selection changes
watch(() => props.tracks, renderTracks, { deep: true })
watch(() => props.selectedTrackIndex, renderTracks)
onMounted(() => {
initMap()
})
onUnmounted(() => {
if (map.value) {
map.value.remove()
}
})
return {
map
}
}
}
</script>