// Budapest Sights Data const sightsData = [ { id: 1, name: "Hungarian Parliament Building", category: "Historic Landmark", description: "One of Europe's most stunning government buildings, located right on the Danube. Features neo-Gothic architecture with over 40 kg of gold, 691 rooms, and 96m height commemorating Hungary's founding year 896.", latitude: 47.5076, longitude: 19.0458, admission: "From €30, guided tours available in English", hours: "Various tour times, advance booking recommended", visit_duration: "60-90 minutes" }, { id: 2, name: "Fisherman's Bastion", category: "Historic Landmark", description: "Fairytale-like neo-Romanesque terrace with seven towers representing the seven Magyar tribes. Offers iconic views of Parliament and the Danube. Lower terrace free 24/7, upper ramparts require ticket.", latitude: 47.5023, longitude: 19.0350, admission: "Lower terrace free, upper ramparts 1,500 HUF (€4)", hours: "Lower 24/7, Upper 9:00-21:00 (summer), 9:00-19:00 (rest of year)", visit_duration: "45-60 minutes" }, { id: 3, name: "Buda Castle", category: "Historic Landmark", description: "UNESCO World Heritage Site, once home to Hungarian kings. Now houses the Hungarian National Gallery and Budapest History Museum. Beautiful courtyards and terraces with stunning views of Pest.", latitude: 47.4967, longitude: 19.0397, admission: "Grounds free, museums require separate tickets", hours: "Grounds 24/7, museums vary", visit_duration: "90-120 minutes" }, { id: 4, name: "Matthias Church", category: "Religious Site", description: "Stunning Gothic church with colorful Zsolnay tiled roof, originally built in 1015. Hosted royal coronations and has excellent acoustics for classical concerts.", latitude: 47.5019, longitude: 19.0348, admission: "3,100 HUF (€8) adults, bell tower extra 3,700 HUF", hours: "Mon-Sat 9:00-17:45, Sun 13:00-17:45", visit_duration: "45-60 minutes" }, { id: 5, name: "Chain Bridge (Széchenyi Lánchíd)", category: "Historic Landmark", description: "Budapest's oldest permanent bridge (1849), first to connect Buda and Pest. Iconic symbol with stone lions, suspension chains, beautiful when illuminated at night.", latitude: 47.4983, longitude: 19.0436, admission: "Free", hours: "24/7", visit_duration: "20-30 minutes" }, { id: 6, name: "St. Stephen's Basilica", category: "Religious Site", description: "Neoclassical church named after Hungary's first king, whose mummified right hand is preserved here. 96m height (same as Parliament). Panoramic dome with 360° views.", latitude: 47.5009, longitude: 19.0534, admission: "6,000 HUF (€15) including dome, 20% discount with Budapest Card", hours: "Mon-Sat 9:00-17:45, Sun 13:00-17:45", visit_duration: "60-90 minutes" }, { id: 7, name: "Heroes' Square", category: "Monument", description: "Iconic square featuring the Millennium Monument with Archangel Gabriel atop central column, surrounded by statues of Hungary's national leaders. Flanked by Museum of Fine Arts and Műcsarnok.", latitude: 47.5146, longitude: 19.0777, admission: "Free", hours: "24/7", visit_duration: "30-45 minutes" }, { id: 8, name: "Széchenyi Thermal Baths", category: "Thermal Bath", description: "Europe's largest medicinal bath complex with 18 pools (3 outdoor). Neo-baroque architecture, waters from 1,246m deep well. Famous for chess players in outdoor pools, even in winter.", latitude: 47.5196, longitude: 19.0815, admission: "From 8,600 HUF (€22) weekdays, 9,200 HUF (€23) weekends", hours: "6:00-22:00 daily", visit_duration: "2-4 hours" }, { id: 9, name: "Gellért Thermal Baths", category: "Thermal Bath", description: "Stunning Art Nouveau baths (1912-1918) at foot of Gellért Hill. Famous for blue Zsolnay tiles, marble columns, stained-glass ceilings. 10 pools with wave pool in summer.", latitude: 47.4826, longitude: 19.0523, admission: "From 17,700 HUF (€45)", hours: "9:00-19:00 daily", visit_duration: "2-3 hours" }, { id: 10, name: "Gellért Hill & Citadel", category: "Viewpoint", description: "235m high hill with panoramic city views. Features Citadel fortress (1851) and Liberty Statue. Easy hike with stunning vistas, especially at sunset.", latitude: 47.4867, longitude: 19.0463, admission: "Free", hours: "24/7", visit_duration: "60-90 minutes" }, { id: 11, name: "Great Market Hall (Central Market)", category: "Market", description: "Largest and oldest indoor market (1897) with neo-Gothic design and Zsolnay tile roof. Three floors with fresh produce, local foods, souvenirs, and traditional Hungarian street food on upper level.", latitude: 47.4870, longitude: 19.0583, admission: "Free entry", hours: "Mon 6:00-17:00, Tue-Fri 6:00-18:00, Sat 6:00-15:00, Sun closed", visit_duration: "60-90 minutes" }, { id: 12, name: "Shoes on the Danube Bank", category: "Memorial", description: "Poignant Holocaust memorial with 60 pairs of iron shoes honoring 3,500 Jews shot into the Danube by Arrow Cross militia (1944-45). Located 300m south of Parliament.", latitude: 47.5050, longitude: 19.0451, admission: "Free", hours: "24/7", visit_duration: "15-30 minutes" }, { id: 13, name: "Dohány Street Synagogue", category: "Religious Site", description: "Europe's largest synagogue (1859) in Moorish Revival style, seats 3,000. Complex includes Heroes' Temple, Jewish Museum, memorial garden. Located at former ghetto border.", latitude: 47.4959, longitude: 19.0618, admission: "Tickets required, guided tours available", hours: "Weekdays only, closed Saturdays and Jewish holidays", visit_duration: "60-90 minutes" }, { id: 14, name: "Hungarian State Opera House", category: "Cultural Venue", description: "Neo-Renaissance masterpiece (1884) by Miklós Ybl on Andrássy Avenue. Interior decorated with 7kg of gold. Offers ballet and opera performances, guided tours available.", latitude: 47.5027, longitude: 19.0586, admission: "Tours and performances vary", hours: "Tour times vary, performances evenings", visit_duration: "45-60 minutes (tour) or 2-3 hours (performance)" }, { id: 15, name: "Szimpla Kert", category: "Ruin Bar", description: "Original and most famous ruin bar (2002) in abandoned building. Eclectic decor with vintage items, multiple rooms, outdoor courtyard. Sunday farmers market. Free concerts.", latitude: 47.4968, longitude: 19.0631, admission: "Free entry, pay for drinks", hours: "Daily 12:00-late, Sunday market 9:00-14:00", visit_duration: "90-120 minutes" }, { id: 16, name: "Vajdahunyad Castle", category: "Historic Landmark", description: "Eclectic castle in City Park blending Romanesque, Gothic, Renaissance, and Baroque styles. Originally built for 1896 Millennium Exhibition, rebuilt in stone due to popularity.", latitude: 47.5148, longitude: 19.0826, admission: "Grounds free, museum inside requires ticket", hours: "Grounds 24/7, museum varies", visit_duration: "45-60 minutes" }, { id: 17, name: "Andrássy Avenue", category: "Street/District", description: "Budapest's most elegant boulevard, UNESCO World Heritage Site. Lined with neo-Renaissance townhouses, luxury shops, cafes. Connects city center to Heroes' Square.", latitude: 47.5035, longitude: 19.0640, admission: "Free", hours: "24/7", visit_duration: "60-90 minutes (walking)" }, { id: 18, name: "Elizabeth Lookout Tower", category: "Viewpoint", description: "Fairytale stone tower on János Hill offering 360° panoramic views of Budapest and surrounding hills. On clear days, can spot Mátra mountains.", latitude: 47.5180, longitude: 18.9570, admission: "Small fee", hours: "Varies by season", visit_duration: "45-60 minutes" }, { id: 19, name: "Váci Street", category: "Shopping Street", description: "Famous pedestrian shopping street running through city center. Filled with shops, restaurants, cafes. Connects to Great Market Hall at southern end.", latitude: 47.4950, longitude: 19.0525, admission: "Free", hours: "24/7 (shops vary)", visit_duration: "60-90 minutes" }, { id: 20, name: "City Park (Városliget)", category: "Park", description: "Large public park with lake, Vajdahunyad Castle, Széchenyi Baths, Heroes' Square. Popular for walking, boating in summer, ice skating in winter.", latitude: 47.5157, longitude: 19.0833, admission: "Free", hours: "24/7", visit_duration: "90-120 minutes" } ]; // App state let map; let markers = []; let selectedSights = new Set(); let hotelMarker = null; let routePolyline = null; let hotelLocation = null; let isPlacingHotel = false; // Constants const WALKING_SPEED_KMH = 5.0; const WALKING_SPEED_MPH = 3.1; const STREET_FACTOR = 1.3; const BUDAPEST_CENTER = [47.4979, 19.0402]; // Category colors for markers const categoryColors = { "Historic Landmark": "#dc2626", "Religious Site": "#fbbf24", "Thermal Bath": "#1e40af", "Viewpoint": "#059669", "Cultural Venue": "#7c3aed", "Market": "#ea580c", "Memorial": "#6b7280", "Ruin Bar": "#0891b2", "Street/District": "#0891b2", "Shopping Street": "#0891b2", "Park": "#059669", "Monument": "#6b7280" }; // Category icons const categoryIcons = { "Historic Landmark": "🏛️", "Religious Site": "⛪", "Thermal Bath": "🛁", "Viewpoint": "🏔️", "Cultural Venue": "🎭", "Market": "🏪", "Memorial": "🕊️", "Ruin Bar": "🍺", "Street/District": "🛣️", "Shopping Street": "🛍️", "Park": "🌳", "Monument": "🗿" }; // Initialize the application document.addEventListener('DOMContentLoaded', function() { initializeMap(); renderSightsList(); setupEventListeners(); showInstructions(); }); // Initialize Leaflet map function initializeMap() { map = L.map('map').setView(BUDAPEST_CENTER, 13); L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { attribution: '© OpenStreetMap contributors', maxZoom: 18 }).addTo(map); // Add all sight markers sightsData.forEach((sight, index) => { addSightMarker(sight, index + 1); }); // Handle map clicks for hotel placement map.on('click', handleMapClick); } // Add sight marker to map function addSightMarker(sight, number) { const category = sight.category; const color = categoryColors[category] || categoryColors["other"]; const icon = categoryIcons[category] || "📍"; const customIcon = L.divIcon({ html: `
${number}
`, className: 'custom-div-icon', iconSize: [30, 30], iconAnchor: [15, 15], popupAnchor: [0, -15] }); const marker = L.marker([sight.latitude, sight.longitude], { icon: customIcon }) .bindPopup(createPopupContent(sight)) .addTo(map); marker.sightData = sight; markers.push(marker); } // Create popup content for markers function createPopupContent(sight) { const icon = categoryIcons[sight.category] || "📍"; return ` `; } // Handle map clicks for hotel placement function handleMapClick(e) { if (isPlacingHotel) { placeHotel(e.latlng); isPlacingHotel = false; document.getElementById('placeHotelBtn').textContent = '📍 Place My Hotel'; document.body.style.cursor = 'default'; } } // Place hotel marker function placeHotel(latlng) { // Remove existing hotel marker if (hotelMarker) { map.removeLayer(hotelMarker); } const hotelIcon = L.divIcon({ html: `
🏨
`, className: 'custom-div-icon', iconSize: [35, 35], iconAnchor: [17.5, 17.5], popupAnchor: [0, -17.5] }); hotelMarker = L.marker(latlng, { icon: hotelIcon }) .bindPopup('

🏨 Your Hotel

Starting point for your route

') .addTo(map); hotelLocation = latlng; // Show hotel info document.getElementById('hotelInfo').classList.remove('hidden'); // Recalculate route if sights are selected if (selectedSights.size > 0) { updateRouteDisplay(); } } // Remove hotel marker function removeHotel() { if (hotelMarker) { map.removeLayer(hotelMarker); hotelMarker = null; hotelLocation = null; document.getElementById('hotelInfo').classList.add('hidden'); updateRouteDisplay(); } } // Render sights list in sidebar function renderSightsList(filter = 'all', searchTerm = '') { const sightsList = document.getElementById('sightsList'); sightsList.innerHTML = ''; const filteredSights = sightsData.filter(sight => { const matchesFilter = filter === 'all' || sight.category === filter || (filter === 'other' && !Object.keys(categoryColors).includes(sight.category)); const matchesSearch = sight.name.toLowerCase().includes(searchTerm.toLowerCase()) || sight.description.toLowerCase().includes(searchTerm.toLowerCase()); return matchesFilter && matchesSearch; }); filteredSights.forEach(sight => { const sightElement = document.createElement('div'); sightElement.className = 'sight-item'; sightElement.innerHTML = createSightItemHTML(sight); sightElement.addEventListener('click', (e) => { if (e.target.type !== 'checkbox') { openSightModal(sight.id); } }); sightsList.appendChild(sightElement); }); } // Create sight item HTML function createSightItemHTML(sight) { const icon = categoryIcons[sight.category] || "📍"; const isSelected = selectedSights.has(sight.id); return `
${icon} ${sight.name}
${sight.category}
${sight.description.substring(0, 120)}...
Duration: ${sight.visit_duration} • Admission: ${sight.admission}
`; } // Toggle sight selection for route planning function toggleSightSelection(sightId) { if (selectedSights.has(sightId)) { selectedSights.delete(sightId); } else { selectedSights.add(sightId); } updateRouteDisplay(); renderSightsList(getCurrentFilter(), getCurrentSearch()); } // Update route display function updateRouteDisplay() { const routeStats = document.getElementById('routeStats'); const selectedSightsContainer = document.getElementById('selectedSights'); const downloadBtn = document.getElementById('downloadRouteBtn'); if (selectedSights.size === 0) { routeStats.classList.add('hidden'); selectedSightsContainer.innerHTML = ''; downloadBtn.classList.add('hidden'); // Remove route polyline if (routePolyline) { map.removeLayer(routePolyline); routePolyline = null; } return; } // Show stats routeStats.classList.remove('hidden'); downloadBtn.classList.remove('hidden'); // Get selected sight objects const selectedSightObjects = Array.from(selectedSights).map(id => sightsData.find(sight => sight.id === id) ); // Calculate route const route = calculateOptimalRoute(selectedSightObjects); const { totalDistance, totalTime } = calculateRouteStats(route); // Update stats display document.getElementById('totalDistance').textContent = `${totalDistance.toFixed(1)} km (${(totalDistance * 0.621371).toFixed(1)} mi)`; document.getElementById('totalTime').textContent = `${Math.round(totalTime)} minutes`; document.getElementById('selectedCount').textContent = `${selectedSights.size} sight${selectedSights.size !== 1 ? 's' : ''}`; // Render selected sights with distances renderSelectedSights(route); // Draw route on map drawRouteOnMap(route); } // Calculate optimal route function calculateOptimalRoute(sights) { if (sights.length === 0) return []; if (sights.length === 1) return sights; let startPoint = hotelLocation ? { latitude: hotelLocation.lat, longitude: hotelLocation.lng } : sights[0]; const route = []; const remaining = [...sights]; let current = startPoint; // If we have a hotel, start from there if (hotelLocation) { route.push({ isHotel: true, ...current }); } // Greedy algorithm: always go to nearest unvisited sight while (remaining.length > 0) { let nearestIndex = 0; let minDistance = calculateDistance( current.latitude, current.longitude, remaining[0].latitude, remaining[0].longitude ); for (let i = 1; i < remaining.length; i++) { const distance = calculateDistance( current.latitude, current.longitude, remaining[i].latitude, remaining[i].longitude ); if (distance < minDistance) { minDistance = distance; nearestIndex = i; } } const nextSight = remaining.splice(nearestIndex, 1)[0]; route.push(nextSight); current = nextSight; } return route; } // Calculate route statistics function calculateRouteStats(route) { if (route.length <= 1) return { totalDistance: 0, totalTime: 0 }; let totalDistance = 0; for (let i = 0; i < route.length - 1; i++) { const current = route[i]; const next = route[i + 1]; const distance = calculateDistance( current.latitude, current.longitude, next.latitude, next.longitude ) * STREET_FACTOR; totalDistance += distance; } const totalTime = (totalDistance / WALKING_SPEED_KMH) * 60; return { totalDistance, totalTime }; } // Calculate distance between two points using Haversine formula function calculateDistance(lat1, lon1, lat2, lon2) { const R = 6371; // Earth's radius in km const dLat = (lat2 - lat1) * Math.PI / 180; const dLon = (lon2 - lon1) * Math.PI / 180; const a = Math.sin(dLat/2) * Math.sin(dLat/2) + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLon/2) * Math.sin(dLon/2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); return R * c; } // Render selected sights in route order function renderSelectedSights(route) { const container = document.getElementById('selectedSights'); container.innerHTML = ''; const sightsOnly = route.filter(item => !item.isHotel); sightsOnly.forEach((sight, index) => { const nextSight = sightsOnly[index + 1]; const prevSight = index === 0 ? (hotelLocation ? { latitude: hotelLocation.lat, longitude: hotelLocation.lng } : sight) : sightsOnly[index - 1]; const distance = calculateDistance( prevSight.latitude, prevSight.longitude, sight.latitude, sight.longitude ) * STREET_FACTOR; const element = document.createElement('div'); element.className = 'selected-sight'; element.innerHTML = `
${index + 1}
${sight.name}
${index === 0 && hotelLocation ? 'From hotel: ' : 'From previous: '} ${distance.toFixed(1)} km (${(distance * 0.621371).toFixed(1)} mi)
`; element.addEventListener('click', () => { map.setView([sight.latitude, sight.longitude], 16); }); container.appendChild(element); }); } // Draw route on map function drawRouteOnMap(route) { // Remove existing route if (routePolyline) { map.removeLayer(routePolyline); } if (route.length < 2) return; const routeCoords = route.map(point => [point.latitude, point.longitude]); routePolyline = L.polyline(routeCoords, { color: '#2563eb', weight: 3, opacity: 0.8, dashArray: '10, 5' }).addTo(map); // Fit map to show all points const group = new L.featureGroup(markers.concat(routePolyline)); if (hotelMarker) { group.addLayer(hotelMarker); } map.fitBounds(group.getBounds().pad(0.1)); } // Open sight detail modal function openSightModal(sightId) { const sight = sightsData.find(s => s.id === sightId); if (!sight) return; const modal = document.getElementById('sightModal'); const title = document.getElementById('modalTitle'); const content = document.getElementById('modalContent'); const checkbox = document.getElementById('modalAddToRoute'); const icon = categoryIcons[sight.category] || "📍"; title.textContent = `${icon} ${sight.name}`; checkbox.checked = selectedSights.has(sight.id); checkbox.onchange = () => toggleSightSelection(sight.id); content.innerHTML = `
Category: ${sight.category}

${sight.description}

💰 Admission: ${sight.admission}
🕒 Hours: ${sight.hours}
⏱️ Recommended Duration: ${sight.visit_duration}
`; modal.classList.remove('hidden'); // Set up zoom button document.getElementById('zoomToSight').onclick = () => { map.setView([sight.latitude, sight.longitude], 16); modal.classList.add('hidden'); }; } // Setup event listeners function setupEventListeners() { // Search document.getElementById('searchInput').addEventListener('input', (e) => { renderSightsList(getCurrentFilter(), e.target.value); }); // Filter buttons document.querySelectorAll('.filter-btn').forEach(btn => { btn.addEventListener('click', (e) => { document.querySelectorAll('.filter-btn').forEach(b => b.classList.remove('active')); e.target.classList.add('active'); renderSightsList(e.target.dataset.category, getCurrentSearch()); }); }); // Hotel placement document.getElementById('placeHotelBtn').addEventListener('click', () => { isPlacingHotel = true; document.getElementById('placeHotelBtn').textContent = '📍 Click on map to place hotel'; document.body.style.cursor = 'crosshair'; }); document.getElementById('removeHotelBtn').addEventListener('click', removeHotel); // Route controls document.getElementById('optimizeRouteBtn').addEventListener('click', updateRouteDisplay); document.getElementById('clearRouteBtn').addEventListener('click', clearRoute); document.getElementById('downloadRouteBtn').addEventListener('click', downloadRoute); // Modal controls document.getElementById('closeModal').addEventListener('click', () => { document.getElementById('sightModal').classList.add('hidden'); }); document.getElementById('closeInstructions').addEventListener('click', () => { document.getElementById('mapInstructions').classList.add('hidden'); }); // Click outside modal to close document.getElementById('sightModal').addEventListener('click', (e) => { if (e.target.id === 'sightModal') { document.getElementById('sightModal').classList.add('hidden'); } }); } // Clear route function clearRoute() { selectedSights.clear(); updateRouteDisplay(); renderSightsList(getCurrentFilter(), getCurrentSearch()); } // Download route as text file function downloadRoute() { if (selectedSights.size === 0) return; const selectedSightObjects = Array.from(selectedSights).map(id => sightsData.find(sight => sight.id === id) ); const route = calculateOptimalRoute(selectedSightObjects); const { totalDistance, totalTime } = calculateRouteStats(route); let content = "BUDAPEST TRAVEL ROUTE\n"; content += "Generated by Budapest Travel Guide\n"; content += "=================================\n\n"; if (hotelLocation) { content += "Starting Point: Your Hotel\n"; content += `Location: ${hotelLocation.lat.toFixed(6)}, ${hotelLocation.lng.toFixed(6)}\n\n`; } content += `Total Distance: ${totalDistance.toFixed(1)} km (${(totalDistance * 0.621371).toFixed(1)} mi)\n`; content += `Estimated Walking Time: ${Math.round(totalTime)} minutes\n`; content += `Number of Sights: ${selectedSights.size}\n\n`; const sightsOnly = route.filter(item => !item.isHotel); sightsOnly.forEach((sight, index) => { content += `${index + 1}. ${sight.name}\n`; content += ` Category: ${sight.category}\n`; content += ` Duration: ${sight.visit_duration}\n`; content += ` Admission: ${sight.admission}\n`; content += ` Hours: ${sight.hours}\n`; content += ` Location: ${sight.latitude.toFixed(6)}, ${sight.longitude.toFixed(6)}\n`; if (index < sightsOnly.length - 1) { const nextSight = sightsOnly[index + 1]; const distance = calculateDistance( sight.latitude, sight.longitude, nextSight.latitude, nextSight.longitude ) * STREET_FACTOR; content += ` → Next: ${distance.toFixed(1)} km (${Math.round((distance / WALKING_SPEED_KMH) * 60)} min walk)\n`; } content += "\n"; }); content += "\nTips:\n"; content += "• Budapest city center is very walkable\n"; content += "• Buda side has hills - budget extra time\n"; content += "• Public transport available for longer distances\n"; content += "• Distances are estimated walking routes\n"; const blob = new Blob([content], { type: 'text/plain' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'budapest-route.txt'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } // Helper functions function getCurrentFilter() { return document.querySelector('.filter-btn.active')?.dataset.category || 'all'; } function getCurrentSearch() { return document.getElementById('searchInput').value; } function showInstructions() { // Hide instructions after 5 seconds setTimeout(() => { document.getElementById('mapInstructions').classList.add('hidden'); }, 5000); } // Make functions available globally for HTML onclick handlers window.toggleSightSelection = toggleSightSelection; window.openSightModal = openSightModal;