geolocation support
This commit is contained in:
		
							
								
								
									
										155
									
								
								app.js
									
									
									
									
									
								
							
							
						
						
									
										155
									
								
								app.js
									
									
									
									
									
								
							@ -58,12 +58,18 @@ let hotelMarker = null;
 | 
				
			|||||||
let routePolyline = null;
 | 
					let routePolyline = null;
 | 
				
			||||||
let hotelLocation = null;
 | 
					let hotelLocation = null;
 | 
				
			||||||
let isPlacingHotel = false;
 | 
					let isPlacingHotel = false;
 | 
				
			||||||
 | 
					let userLocationMarker = null;
 | 
				
			||||||
 | 
					let userLocationCircle = null;
 | 
				
			||||||
 | 
					let gpsWatchId = null;
 | 
				
			||||||
 | 
					let hasInitialGpsFix = false;
 | 
				
			||||||
 | 
					let lastKnownLocation = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Constants
 | 
					// Constants
 | 
				
			||||||
const WALKING_SPEED_KMH = 5.0;
 | 
					const WALKING_SPEED_KMH = 5.0;
 | 
				
			||||||
const WALKING_SPEED_MPH = 3.1;
 | 
					const WALKING_SPEED_MPH = 3.1;
 | 
				
			||||||
const STREET_FACTOR = 1.3;
 | 
					const STREET_FACTOR = 1.3;
 | 
				
			||||||
const BUDAPEST_CENTER = [47.4979, 19.0402];
 | 
					const BUDAPEST_CENTER = [47.4979, 19.0402];
 | 
				
			||||||
 | 
					const GPS_FOCUS_ZOOM = 15;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Category colors for markers
 | 
					// Category colors for markers
 | 
				
			||||||
const categoryColors = {
 | 
					const categoryColors = {
 | 
				
			||||||
@ -109,6 +115,7 @@ const categoryIcons = {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
// Initialize the application
 | 
					// Initialize the application
 | 
				
			||||||
document.addEventListener('DOMContentLoaded', function() {
 | 
					document.addEventListener('DOMContentLoaded', function() {
 | 
				
			||||||
 | 
					  setGpsButtonState('idle');
 | 
				
			||||||
  initializeMap();
 | 
					  initializeMap();
 | 
				
			||||||
  renderSightsList();
 | 
					  renderSightsList();
 | 
				
			||||||
  setupEventListeners();
 | 
					  setupEventListeners();
 | 
				
			||||||
@ -185,6 +192,142 @@ function handleMapClick(e) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// 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
 | 
					// Place hotel marker
 | 
				
			||||||
function placeHotel(latlng) {
 | 
					function placeHotel(latlng) {
 | 
				
			||||||
  // Remove existing hotel marker
 | 
					  // Remove existing hotel marker
 | 
				
			||||||
@ -561,6 +704,12 @@ function setupEventListeners() {
 | 
				
			|||||||
  document.getElementById('clearRouteBtn').addEventListener('click', clearRoute);
 | 
					  document.getElementById('clearRouteBtn').addEventListener('click', clearRoute);
 | 
				
			||||||
  document.getElementById('downloadRouteBtn').addEventListener('click', downloadRoute);
 | 
					  document.getElementById('downloadRouteBtn').addEventListener('click', downloadRoute);
 | 
				
			||||||
  
 | 
					  
 | 
				
			||||||
 | 
					  // GPS control
 | 
				
			||||||
 | 
					  const gpsButton = document.getElementById('gpsToggle');
 | 
				
			||||||
 | 
					  if (gpsButton) {
 | 
				
			||||||
 | 
					    gpsButton.addEventListener('click', handleGpsButtonClick);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
  // Modal controls
 | 
					  // Modal controls
 | 
				
			||||||
  document.getElementById('closeModal').addEventListener('click', () => {
 | 
					  document.getElementById('closeModal').addEventListener('click', () => {
 | 
				
			||||||
    document.getElementById('sightModal').classList.add('hidden');
 | 
					    document.getElementById('sightModal').classList.add('hidden');
 | 
				
			||||||
@ -578,6 +727,12 @@ function setupEventListeners() {
 | 
				
			|||||||
  });
 | 
					  });
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					window.addEventListener('beforeunload', () => {
 | 
				
			||||||
 | 
					  if (gpsWatchId !== null && 'geolocation' in navigator) {
 | 
				
			||||||
 | 
					    navigator.geolocation.clearWatch(gpsWatchId);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Clear route
 | 
					// Clear route
 | 
				
			||||||
function clearRoute() {
 | 
					function clearRoute() {
 | 
				
			||||||
  selectedSights.clear();
 | 
					  selectedSights.clear();
 | 
				
			||||||
 | 
				
			|||||||
@ -105,6 +105,13 @@
 | 
				
			|||||||
            <!-- Map Container -->
 | 
					            <!-- Map Container -->
 | 
				
			||||||
            <div class="map-container">
 | 
					            <div class="map-container">
 | 
				
			||||||
                <div id="map"></div>
 | 
					                <div id="map"></div>
 | 
				
			||||||
 | 
					                <button id="gpsToggle" class="gps-control" type="button" aria-label="Show my location" aria-pressed="false" title="Show my location">
 | 
				
			||||||
 | 
					                    <svg class="gps-control__icon" viewBox="0 0 24 24" role="img" aria-hidden="true">
 | 
				
			||||||
 | 
					                        <circle cx="12" cy="12" r="3" fill="currentColor"></circle>
 | 
				
			||||||
 | 
					                        <circle cx="12" cy="12" r="8" fill="none" stroke="currentColor" stroke-width="1.8"></circle>
 | 
				
			||||||
 | 
					                        <path d="M12 4V2M12 22v-2M4 12H2M22 12h-2" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"></path>
 | 
				
			||||||
 | 
					                    </svg>
 | 
				
			||||||
 | 
					                </button>
 | 
				
			||||||
                <div id="mapInstructions" class="map-instructions">
 | 
					                <div id="mapInstructions" class="map-instructions">
 | 
				
			||||||
                    <div class="instruction-content">
 | 
					                    <div class="instruction-content">
 | 
				
			||||||
                        <h4>🗺️ How to Use This Map</h4>
 | 
					                        <h4>🗺️ How to Use This Map</h4>
 | 
				
			||||||
 | 
				
			|||||||
							
								
								
									
										44
									
								
								style.css
									
									
									
									
									
								
							
							
						
						
									
										44
									
								
								style.css
									
									
									
									
									
								
							@ -1288,6 +1288,50 @@ p {
 | 
				
			|||||||
  border-radius: var(--radius-base);
 | 
					  border-radius: var(--radius-base);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.gps-control {
 | 
				
			||||||
 | 
					  position: absolute;
 | 
				
			||||||
 | 
					  bottom: var(--space-16);
 | 
				
			||||||
 | 
					  right: var(--space-16);
 | 
				
			||||||
 | 
					  width: 44px;
 | 
				
			||||||
 | 
					  height: 44px;
 | 
				
			||||||
 | 
					  border: none;
 | 
				
			||||||
 | 
					  border-radius: 50%;
 | 
				
			||||||
 | 
					  background: rgba(var(--color-slate-500-rgb), 0.25);
 | 
				
			||||||
 | 
					  color: var(--color-slate-500);
 | 
				
			||||||
 | 
					  display: flex;
 | 
				
			||||||
 | 
					  align-items: center;
 | 
				
			||||||
 | 
					  justify-content: center;
 | 
				
			||||||
 | 
					  cursor: pointer;
 | 
				
			||||||
 | 
					  box-shadow: var(--shadow-md);
 | 
				
			||||||
 | 
					  transition: background var(--duration-fast) var(--ease-standard),
 | 
				
			||||||
 | 
					    color var(--duration-fast) var(--ease-standard),
 | 
				
			||||||
 | 
					    transform var(--duration-fast) var(--ease-standard);
 | 
				
			||||||
 | 
					  z-index: 1100;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.gps-control:hover {
 | 
				
			||||||
 | 
					  transform: translateY(-1px);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.gps-control:focus-visible {
 | 
				
			||||||
 | 
					  outline: none;
 | 
				
			||||||
 | 
					  box-shadow: var(--shadow-md), var(--focus-ring);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.gps-control--pending {
 | 
				
			||||||
 | 
					  background: rgba(var(--color-slate-500-rgb), 0.4);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.gps-control--active {
 | 
				
			||||||
 | 
					  background: var(--color-primary);
 | 
				
			||||||
 | 
					  color: var(--color-btn-primary-text);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.gps-control__icon {
 | 
				
			||||||
 | 
					  width: 22px;
 | 
				
			||||||
 | 
					  height: 22px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.map-instructions {
 | 
					.map-instructions {
 | 
				
			||||||
  position: absolute;
 | 
					  position: absolute;
 | 
				
			||||||
  top: var(--space-20);
 | 
					  top: var(--space-20);
 | 
				
			||||||
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user