824 lines
41 KiB
JavaScript
824 lines
41 KiB
JavaScript
// Budapest Sights Data
|
|
const sightsData = [
|
|
// Historic Landmarks & Original Top Sights (IDs 1-20)
|
|
{ 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 from 13th century. Now houses the Hungarian National Gallery and Budapest History Museum. Beautiful courtyards and terraces with stunning views of Pest. Accessible by funicular railway.", 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: "Bridge", description: "Budapest's oldest permanent bridge from 1840s (opened 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. Home to tethered balloon rides (150m altitude).", latitude: 47.5157, longitude: 19.0833, admission: "Free", hours: "24/7", visit_duration: "90-120 minutes" },
|
|
|
|
// NEW MUSEUMS (IDs 21-26)
|
|
{ id: 21, name: "Museum of Fine Arts", category: "Museum", description: "One of Europe's finest art museums at Heroes' Square. Built 1900-1906 in neo-classical style. Houses 100,000+ pieces including works by Raphael, Picasso, El Greco, Cézanne. Features Egyptian, Greek, Roman collections and Hungarian art from pre-1800.", latitude: 47.5159, longitude: 19.0764, admission: "Varies by exhibition, check website", hours: "Tue-Sun 10:00-18:00, closed Mondays", visit_duration: "120-180 minutes" },
|
|
{ id: 22, name: "Pinball Museum", category: "Museum", description: "Europe's largest interactive pinball exhibition with 160+ machines. All games playable with admission. Collection includes rarities from 1940s including first pinball with flippers (Humpty Dumpty, 1947). Arcade games, retro games, Star Wars machines. At Radnóti Miklós utca near Margaret Island.", latitude: 47.5108, longitude: 19.0542, admission: "5,500 HUF (€14) adults, 4,000 HUF under 26/over 62, unlimited play", hours: "Wed-Fri 16:00-00:00, Sat 13:00-00:00, Sun 11:00-22:00, closed Mon-Tue", visit_duration: "90-180 minutes" },
|
|
{ id: 23, name: "Museum of Illusions", category: "Museum", description: "Interactive museum with mind-bending optical illusions and perspective-altering exhibits. Features Ames Room, Anti-Gravity Room, Vortex Tunnel, Infinity Room. Educational and fun for all ages. Located on Bajcsy-Zsilinszky street near Deák Square.", latitude: 47.5024, longitude: 19.0526, admission: "5,400 HUF (€14) adults, 3,900 HUF students/seniors", hours: "Sun-Thu 10:00-19:00, Fri-Sat 10:00-20:00", visit_duration: "45-60 minutes" },
|
|
{ id: 24, name: "Cat Museum Budapest", category: "Museum", description: "Charming interactive museum combining cat-themed art, historical exhibits, and friendly resident cats. Visitors can relax, play with well-cared-for cats, and learn about feline history. Small, cozy atmosphere on Vadász utca. Includes free hot drink.", latitude: 47.4982, longitude: 19.0518, admission: "Entry ticket required, check website for prices", hours: "Daily 11:00-18:30", visit_duration: "60-90 minutes" },
|
|
{ id: 25, name: "IKONO Budapest", category: "Museum", description: "Immersive art experience with 12 interactive rooms. Features Endless Lanterns, Light Painting studio, and Koketit's self-love artwork. Blends art, play, and creativity. Perfect for all ages, Instagram-worthy installations. Located on Váci Street.", latitude: 47.4960, longitude: 19.0540, admission: "Ticket prices vary, book online recommended", hours: "Sun-Thu 10:00-21:00, Fri-Sat 10:00-22:00", visit_duration: "60-90 minutes" },
|
|
{ id: 26, name: "Hungarian Money Museum", category: "Museum", description: "Interactive museum by Hungarian National Bank exploring money history and financial education. Features rare coins including 1603 five-round gold ducat, security features of Hungarian banknotes, gold bar lifting, and stock market simulations. 1,000 sqm with 200+ displays at Krisztina körút 6.", latitude: 47.4995, longitude: 19.0246, admission: "Check website for current prices", hours: "Mon 9:00-17:00, Tue closed, Wed-Fri 9:00-17:00, Thu until 19:00, Sat-Sun 10:00-18:00", visit_duration: "90-120 minutes" },
|
|
|
|
// NEW ACTIVITIES (IDs 27-30)
|
|
{ id: 27, name: "Budapest Eye (Ferris Wheel)", category: "Activity", description: "Europe's tallest mobile Ferris wheel at 65 meters height on Erzsébet Square. 42 air-conditioned enclosed cabins with panoramic 360° views of Budapest landmarks - Parliament, Buda Castle, Chain Bridge, St. Stephen's Basilica. One ride equals 3 turns, 8-10 minutes. Stunning at night with illumination.", latitude: 47.4988, longitude: 19.0542, admission: "4,300 HUF (€11) adults, 2,300 HUF (€6) children 2-12, 300 HUF babies", hours: "Mon-Thu 11:00-23:00, Fri-Sat 11:00-00:00, Sun 11:00-23:00", visit_duration: "30-45 minutes including wait time" },
|
|
{ id: 28, name: "Tram Line 2 Ride", category: "Activity", description: "Scenic tram journey along the Danube from Great Market Hall to Margaret Bridge. UNESCO World Heritage panorama with views of Parliament, Buda Castle, Chain Bridge. One of the world's most beautiful tram routes. Use standard BKK public transport ticket.", latitude: 47.4870, longitude: 19.0583, admission: "Standard BKK ticket (350 HUF single, ~€1)", hours: "Trams run approximately 4:30-23:30 daily", visit_duration: "20-30 minutes one way" },
|
|
{ id: 29, name: "Tethered Balloon in City Park", category: "Activity", description: "Helium balloon ride ascending to 150 meters in Városliget (City Park). Offers breathtaking aerial views of Budapest while safely tethered. Weather-dependent activity, provides unique perspective of Heroes' Square, Vajdahunyad Castle, and city skyline.", latitude: 47.5165, longitude: 19.0850, admission: "Prices vary, check on-site or book online", hours: "Weather dependent, typically daytime hours in good conditions", visit_duration: "30-45 minutes" },
|
|
{ id: 30, name: "Cinema Mystica", category: "Activity", description: "Museum of Lights and Magic with immersive audiovisual experiences. 10 rooms, 23 installations spanning 1,000 sqm with 100+ projectors. Features digital art, 3D-printed sculptures, projection-mapped spaces, interactive installations. Located in Párisi Udvar downtown.", latitude: 47.4968, longitude: 19.0547, admission: "6,700 HUF (€17) adults, 5,000 HUF students/seniors, 4,000 HUF children 3-12", hours: "Daily 10:00-22:00", visit_duration: "60-90 minutes" },
|
|
|
|
// CAFÉS AND RESTAURANTS (IDs 31-36)
|
|
{ id: 31, name: "Café Gerbeaud", category: "Café", description: "Historic café (1858) on Vörösmarty Square, symbol of Budapest elegance. Over 100 cake varieties. Famous for Dobos torte (5-layer chocolate buttercream with caramel), which Empress Sisi loved. Also try Hungarian Classics plate. Golden décor, heavy curtains, chandeliers. Terrace facing square.", latitude: 47.4963, longitude: 19.0530, admission: "Cake slices 3,450-3,650 HUF (€9-10), 50% discount for takeaway", hours: "Mon-Thu 9:00-20:00, Fri-Sat 9:00-21:00, Sun 9:00-20:00", visit_duration: "45-90 minutes" },
|
|
{ id: 32, name: "Café New York", category: "Café", description: "World's most beautiful café (opened 1894) in neo-Renaissance style. Marble columns, crystal chandeliers, ceiling frescoes, gilded details. Historic meeting place for writers, poets, artists. Try Eszterházy torte (almond meringue with cognac buttercream). Live Gypsy music from 11am. Reservation recommended for dinner after 6pm.", latitude: 47.5071, longitude: 19.0705, admission: "Cake slices from 4,000 HUF (€10), main dishes higher", hours: "Mon-Wed 7:00-00:00, Thu-Sun 7:00-02:30", visit_duration: "60-120 minutes" },
|
|
{ id: 33, name: "Restaurant Gundel", category: "Restaurant", description: "Hungary's most famous restaurant (since 1894) in City Park next to zoo. 130+ years of tradition serving classic Hungarian cuisine. Features 'National 11' - eleven iconic Gundel dishes representing Hungarian gastronomy. Created by Károly Gundel. Elegant dining in historic setting with garden terrace.", latitude: 47.5165, longitude: 19.0837, admission: "Fine dining prices, reservations recommended", hours: "Daily, lunch and dinner service", visit_duration: "90-180 minutes" },
|
|
{ id: 34, name: "Falk Miksa Street", category: "Shopping Street", description: "Street of Antiques. Budapest's antique quarter with numerous shops selling vintage furniture, art, porcelain, jewelry, books. Perfect for collectors and those seeking unique Hungarian treasures. Runs parallel to Parliament between Margaret Bridge and Jászai Mari Square.", latitude: 47.5080, longitude: 19.0480, admission: "Free to browse, shop prices vary", hours: "Shop hours vary, typically 10:00-18:00 weekdays", visit_duration: "60-90 minutes" },
|
|
{ id: 35, name: "Felix Kitchen & Bar", category: "Restaurant", description: "Modern Hungarian restaurant and bar with contemporary atmosphere. Popular dining spot offering fusion of traditional and modern Hungarian cuisine. Located in central Budapest, good for casual dining or drinks.", latitude: 47.4985, longitude: 19.0540, admission: "Restaurant prices, moderate range", hours: "Daily, lunch and dinner service", visit_duration: "60-120 minutes" },
|
|
{ id: 36, name: "Lehel Market", category: "Market", description: "Local market alternative to Great Market Hall. Less touristy, authentic Budapest market experience. Fresh produce, meats, baked goods, Hungarian specialties. Art Nouveau building with distinctive architecture. Popular with locals near Lehel tér metro station (M3 line).", latitude: 47.5186, longitude: 19.0646, admission: "Free entry", hours: "Mon 6:00-17:00, Tue-Fri 6:00-18:00, Sat 6:00-14:00, Sun closed", visit_duration: "45-60 minutes" },
|
|
|
|
// BRIDGES AND PARKS (IDs 37-39)
|
|
{ id: 37, name: "Margaret Bridge", category: "Bridge", description: "Second permanent bridge in Budapest (1872-1876), designed by French engineer. 607 meters long, connects Buda and Pest with unique branch to Margaret Island. French-style elegant design with 7 pillars decorated with sculptures. Carries trams 4 and 6.", latitude: 47.5155, longitude: 19.0454, admission: "Free", hours: "24/7", visit_duration: "15-20 minutes to cross" },
|
|
{ id: 38, name: "Liberty Bridge (Szabadság híd)", category: "Bridge", description: "Art Nouveau bridge (1894-1896) painted distinctive green. Features mythical Turul bird statues at ends. Shorter than Chain Bridge at 333m. Connects Gellért Baths on Buda side with Great Market Hall on Pest side. Popular photo spot, pedestrian-friendly.", latitude: 47.4868, longitude: 19.0544, admission: "Free", hours: "24/7", visit_duration: "15-20 minutes" },
|
|
{ id: 39, name: "Margaret Island", category: "Park", description: "2.5km green oasis in Danube River. Car-free paradise with gardens, running tracks, medieval ruins (13th-century monastery, Franciscan church, Romanesque tower). Features Musical Fountain, Japanese Garden, Rose Garden, Palatinus Water Park. Popular for jogging, cycling, picnics. Named after King Béla IV's daughter.", latitude: 47.5280, longitude: 19.0485, admission: "Free (water park separate fee)", hours: "24/7 access, attractions vary", visit_duration: "120-240 minutes" }
|
|
];
|
|
|
|
// App state
|
|
let map;
|
|
let markers = [];
|
|
let selectedSights = new Set();
|
|
let hotelMarker = null;
|
|
let routePolyline = null;
|
|
let hotelLocation = null;
|
|
let isPlacingHotel = false;
|
|
let userLocationMarker = null;
|
|
let userLocationCircle = null;
|
|
let gpsWatchId = null;
|
|
let hasInitialGpsFix = false;
|
|
let lastKnownLocation = null;
|
|
|
|
// 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];
|
|
const GPS_FOCUS_ZOOM = 15;
|
|
|
|
// Category colors for markers
|
|
const categoryColors = {
|
|
"Historic Landmark": "#dc2626",
|
|
"Religious Site": "#fbbf24",
|
|
"Thermal Bath": "#1e40af",
|
|
"Viewpoint": "#059669",
|
|
"Cultural Venue": "#7c3aed",
|
|
"Market": "#ea580c",
|
|
"Memorial": "#6b7280",
|
|
"Museum": "#0d9488",
|
|
"Activity": "#10b981",
|
|
"Café": "#a16207",
|
|
"Restaurant": "#db2777",
|
|
"Ruin Bar": "#b91c1c",
|
|
"Street/District": "#0284c7",
|
|
"Shopping Street": "#0284c7",
|
|
"Park": "#15803d",
|
|
"Bridge": "#475569",
|
|
"Monument": "#6b7280"
|
|
};
|
|
|
|
// Category icons
|
|
const categoryIcons = {
|
|
"Historic Landmark": "🏛️",
|
|
"Religious Site": "⛪",
|
|
"Thermal Bath": "🛁",
|
|
"Viewpoint": "🏔️",
|
|
"Cultural Venue": "🎭",
|
|
"Market": "🏪",
|
|
"Memorial": "🕊️",
|
|
"Museum": "🏛️",
|
|
"Activity": "🎯",
|
|
"Café": "☕",
|
|
"Restaurant": "🍽️",
|
|
"Ruin Bar": "🍺",
|
|
"Street/District": "🛣️",
|
|
"Shopping Street": "🛍️",
|
|
"Park": "🌳",
|
|
"Bridge": "🌉",
|
|
"Monument": "🗿"
|
|
};
|
|
|
|
// Initialize the application
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
setGpsButtonState('idle');
|
|
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: `<div class="custom-marker" style="background-color: ${color}; color: white; border-radius: 50%; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 12px; border: 2px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">${number}</div>`,
|
|
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] || "📍";
|
|
const isSelected = selectedSights.has(sight.id);
|
|
return `
|
|
<div class="popup-content">
|
|
<h4>${icon} ${sight.name}</h4>
|
|
<p><strong>Category:</strong> ${sight.category}</p>
|
|
<p><strong>Admission:</strong> ${sight.admission}</p>
|
|
<p><strong>Hours:</strong> ${sight.hours}</p>
|
|
<p><strong>Duration:</strong> ${sight.visit_duration}</p>
|
|
<label class="checkbox-wrapper" style="margin: 8px 0; display: block;">
|
|
<input type="checkbox" ${isSelected ? 'checked' : ''} onchange="toggleSightSelection(${sight.id})">
|
|
<span>Add to my route</span>
|
|
</label>
|
|
<button onclick="openSightModal(${sight.id})" class="btn btn--primary btn--sm">More Details</button>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// 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';
|
|
}
|
|
}
|
|
|
|
// GPS button state manager
|
|
function setGpsButtonState(state) {
|
|
const gpsButton = document.getElementById('gpsToggle');
|
|
if (!gpsButton) return;
|
|
|
|
gpsButton.classList.remove('gps-control--active', 'gps-control--pending');
|
|
gpsButton.dataset.state = state;
|
|
|
|
if (state === 'active') {
|
|
gpsButton.classList.add('gps-control--active');
|
|
gpsButton.setAttribute('aria-pressed', 'true');
|
|
gpsButton.setAttribute('title', 'Center map on my location');
|
|
} else if (state === 'pending') {
|
|
gpsButton.classList.add('gps-control--pending');
|
|
gpsButton.setAttribute('aria-pressed', 'true');
|
|
gpsButton.setAttribute('title', 'Locating...');
|
|
} else {
|
|
gpsButton.setAttribute('aria-pressed', 'false');
|
|
gpsButton.setAttribute('title', 'Show my location');
|
|
}
|
|
}
|
|
|
|
// Handle GPS toggle clicks
|
|
function handleGpsButtonClick() {
|
|
if (!gpsWatchId) {
|
|
startGpsTracking();
|
|
return;
|
|
}
|
|
|
|
if (hasInitialGpsFix && lastKnownLocation) {
|
|
map.setView(lastKnownLocation, Math.max(map.getZoom(), GPS_FOCUS_ZOOM), {
|
|
animate: true
|
|
});
|
|
} else {
|
|
setGpsButtonState('pending');
|
|
}
|
|
}
|
|
|
|
function startGpsTracking() {
|
|
if (!('geolocation' in navigator)) {
|
|
alert('Geolocation is not supported by your browser.');
|
|
return;
|
|
}
|
|
|
|
setGpsButtonState('pending');
|
|
|
|
gpsWatchId = navigator.geolocation.watchPosition(
|
|
handleGpsSuccess,
|
|
handleGpsError,
|
|
{
|
|
enableHighAccuracy: true,
|
|
maximumAge: 10000,
|
|
timeout: 15000
|
|
}
|
|
);
|
|
}
|
|
|
|
function handleGpsSuccess(position) {
|
|
const { latitude, longitude, accuracy } = position.coords;
|
|
const latlng = [latitude, longitude];
|
|
|
|
lastKnownLocation = latlng;
|
|
|
|
if (!userLocationMarker) {
|
|
userLocationMarker = L.circleMarker(latlng, {
|
|
radius: 8,
|
|
fillColor: '#2563eb',
|
|
color: '#ffffff',
|
|
weight: 2,
|
|
opacity: 1,
|
|
fillOpacity: 1,
|
|
pane: 'markerPane'
|
|
}).addTo(map);
|
|
} else {
|
|
userLocationMarker.setLatLng(latlng);
|
|
}
|
|
|
|
if (!userLocationCircle) {
|
|
userLocationCircle = L.circle(latlng, {
|
|
radius: accuracy,
|
|
color: '#2563eb',
|
|
weight: 1,
|
|
opacity: 0.6,
|
|
fillColor: '#3b82f6',
|
|
fillOpacity: 0.15
|
|
}).addTo(map);
|
|
} else {
|
|
userLocationCircle.setLatLng(latlng);
|
|
userLocationCircle.setRadius(accuracy);
|
|
}
|
|
|
|
if (!hasInitialGpsFix) {
|
|
hasInitialGpsFix = true;
|
|
map.setView(latlng, Math.max(map.getZoom(), GPS_FOCUS_ZOOM));
|
|
}
|
|
|
|
setGpsButtonState('active');
|
|
}
|
|
|
|
function handleGpsError(error) {
|
|
console.warn('Geolocation error:', error);
|
|
|
|
if (error.code === error.PERMISSION_DENIED) {
|
|
stopGpsTracking(true);
|
|
alert('Location access was denied. Enable location permissions to display your position.');
|
|
return;
|
|
}
|
|
|
|
if (!hasInitialGpsFix) {
|
|
setGpsButtonState('idle');
|
|
alert('Unable to determine your location. Please try again.');
|
|
}
|
|
}
|
|
|
|
function stopGpsTracking(clearLayers = false) {
|
|
if (gpsWatchId !== null) {
|
|
navigator.geolocation.clearWatch(gpsWatchId);
|
|
gpsWatchId = null;
|
|
}
|
|
|
|
if (clearLayers) {
|
|
if (userLocationMarker) {
|
|
map.removeLayer(userLocationMarker);
|
|
userLocationMarker = null;
|
|
}
|
|
if (userLocationCircle) {
|
|
map.removeLayer(userLocationCircle);
|
|
userLocationCircle = null;
|
|
}
|
|
hasInitialGpsFix = false;
|
|
lastKnownLocation = null;
|
|
}
|
|
|
|
setGpsButtonState('idle');
|
|
}
|
|
|
|
// Place hotel marker
|
|
function placeHotel(latlng) {
|
|
// Remove existing hotel marker
|
|
if (hotelMarker) {
|
|
map.removeLayer(hotelMarker);
|
|
}
|
|
|
|
const hotelIcon = L.divIcon({
|
|
html: `<div class="custom-marker" style="background-color: #ec4899; color: white; border-radius: 50%; width: 35px; height: 35px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 16px; border: 2px solid white; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">🏨</div>`,
|
|
className: 'custom-div-icon',
|
|
iconSize: [35, 35],
|
|
iconAnchor: [17.5, 17.5],
|
|
popupAnchor: [0, -17.5]
|
|
});
|
|
|
|
hotelMarker = L.marker(latlng, { icon: hotelIcon })
|
|
.bindPopup('<div><h4>🏨 Your Hotel</h4><p>Starting point for your route</p></div>')
|
|
.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 `
|
|
<div class="sight-item-header">
|
|
<div>
|
|
<div class="sight-item-title">${icon} ${sight.name}</div>
|
|
<div class="sight-category">${sight.category}</div>
|
|
</div>
|
|
<input type="checkbox" class="sight-checkbox"
|
|
${isSelected ? 'checked' : ''}
|
|
onchange="toggleSightSelection(${sight.id})">
|
|
</div>
|
|
<div class="sight-description">${sight.description.substring(0, 120)}...</div>
|
|
<div class="sight-details">
|
|
<strong>Duration:</strong> ${sight.visit_duration} •
|
|
<strong>Admission:</strong> ${sight.admission}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
// Toggle sight selection for route planning
|
|
function toggleSightSelection(sightId) {
|
|
if (selectedSights.has(sightId)) {
|
|
selectedSights.delete(sightId);
|
|
} else {
|
|
selectedSights.add(sightId);
|
|
}
|
|
|
|
// Update the popup content for this sight's marker
|
|
const marker = markers.find(m => m.sightData && m.sightData.id === sightId);
|
|
if (marker) {
|
|
marker.setPopupContent(createPopupContent(marker.sightData));
|
|
}
|
|
|
|
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 = `
|
|
<div class="sight-order">${index + 1}</div>
|
|
<div class="selected-sight-info">
|
|
<div class="selected-sight-name">${sight.name}</div>
|
|
<div class="selected-sight-distance">
|
|
${index === 0 && hotelLocation ? 'From hotel: ' : 'From previous: '}
|
|
${distance.toFixed(1)} km (${(distance * 0.621371).toFixed(1)} mi)
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
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 = `
|
|
<div style="margin-bottom: 16px;">
|
|
<strong>Category:</strong> ${sight.category}
|
|
</div>
|
|
<div style="margin-bottom: 16px;">
|
|
<p>${sight.description}</p>
|
|
</div>
|
|
<div style="margin-bottom: 12px;">
|
|
<strong>💰 Admission:</strong> ${sight.admission}
|
|
</div>
|
|
<div style="margin-bottom: 12px;">
|
|
<strong>🕒 Hours:</strong> ${sight.hours}
|
|
</div>
|
|
<div style="margin-bottom: 12px;">
|
|
<strong>⏱️ Recommended Duration:</strong> ${sight.visit_duration}
|
|
</div>
|
|
`;
|
|
|
|
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);
|
|
|
|
// GPS control
|
|
const gpsButton = document.getElementById('gpsToggle');
|
|
if (gpsButton) {
|
|
gpsButton.addEventListener('click', handleGpsButtonClick);
|
|
}
|
|
|
|
// 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');
|
|
}
|
|
});
|
|
}
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
if (gpsWatchId !== null && 'geolocation' in navigator) {
|
|
navigator.geolocation.clearWatch(gpsWatchId);
|
|
}
|
|
});
|
|
|
|
// 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;
|