171 lines
5.6 KiB
Vue
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: '© <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> |