Geolocation — User Location and Map Centering

Why Geolocation Matters

For a camping map, knowing where the user is located is the most important feature. Users want to find campsites near them, get directions, and see how far they are from potential camping spots. The Geolocation API provides this capability in any modern browser.

Why this matters for your career:

  • Geolocation is a core feature of location-based applications
  • Understanding the Geolocation API is essential for map, delivery, and travel apps
  • Handling permissions and errors gracefully is a mark of good UX
  • The Geolocation API is simple but has important edge cases

What Is the Geolocation API?

The Geolocation API provides access to the user's geographical location through the browser. It works on both desktop and mobile devices using GPS (mobile), Wi-Fi positioning, or IP-based approximation.

Key Methods

| Method | Description | |--------|-------------| | getCurrentPosition() | One-time location request | | watchPosition() | Continuous location tracking | | clearWatch() | Stop tracking |

Getting User Location

function getUserLocation() {
  if (!navigator.geolocation) {
    console.log('Geolocation is not supported by this browser.');
    // Fall back to IP-based location
    return fetchIPLocation();
  }

  navigator.geolocation.getCurrentPosition(
    // Success callback
    (position) => {
      const lat = position.coords.latitude;
      const lng = position.coords.longitude;
      const accuracy = position.coords.accuracy;

      console.log(`Latitude: ${lat}, Longitude: ${lng}`);
      console.log(`Accuracy: ${accuracy} meters`);

      // Center the map on user location
      map.setView([lat, lng], 12);

      // Add a user location marker
      addUserMarker(lat, lng);
    },
    // Error callback
    (error) => {
      switch(error.code) {
        case error.PERMISSION_DENIED:
          console.log('User denied the request for Geolocation.');
          break;
        case error.POSITION_UNAVAILABLE:
          console.log('Location information is unavailable.');
          break;
        case error.TIMEOUT:
          console.log('The request to get user location timed out.');
          break;
        case error.UNKNOWN_ERROR:
          console.log('An unknown error occurred.');
          break;
      }
      // Fall back to default location (Taiwan center)
      map.setView([23.5, 121.0], 8);
    },
    // Options
    {
      enableHighAccuracy: true,  // Use GPS if available
      timeout: 10000,            // Wait up to 10 seconds
      maximumAge: 300000         // Cache location for 5 minutes
    }
  );
}

Tracking Location Continuously

let watchId = null;

function startTracking() {
  if (!navigator.geolocation) {
    alert('Geolocation is not supported.');
    return;
  }

  watchId = navigator.geolocation.watchPosition(
    (position) => {
      const lat = position.coords.latitude;
      const lng = position.coords.longitude;

      // Update user marker position
      updateUserMarker(lat, lng);

      // Update distance to selected campsite
      if (selectedCampsite) {
        updateDistanceToCampsite(lat, lng, selectedCampsite);
      }
    },
    (error) => console.error('Tracking error:', error),
    {
      enableHighAccuracy: true,
      timeout: 5000,
      maximumAge: 0  // Always get fresh location
    }
  );
}

function stopTracking() {
  if (watchId) {
    navigator.geolocation.clearWatch(watchId);
    watchId = null;
  }
}

Displaying User Location on Map

let userMarker = null;
let userAccuracyCircle = null;

function addUserMarker(lat, lng, accuracy) {
  // Remove previous marker and accuracy circle
  if (userMarker) map.removeLayer(userMarker);
  if (userAccuracyCircle) map.removeLayer(userAccuracyCircle);

  // Blue dot for user location
  userMarker = L.circleMarker([lat, lng], {
    radius: 8,
    fillColor: '#3b82f6',
    color: '#1d4ed8',
    weight: 3,
    fillOpacity: 1
  }).addTo(map);

  // Accuracy circle (shows how precise the location is)
  if (accuracy) {
    userAccuracyCircle = L.circle([lat, lng], {
      radius: accuracy,
      color: '#3b82f6',
      fillColor: '#3b82f6',
      fillOpacity: 0.1,
      weight: 1
    }).addTo(map);
  }

  userMarker.bindPopup('<b>You are here</b>');
}

function updateUserMarker(lat, lng) {
  if (userMarker) {
    userMarker.setLatLng([lat, lng]);
  }
  if (userAccuracyCircle) {
    userAccuracyCircle.setLatLng([lat, lng]);
  }
}

Calculating Distance to Campsites

function calculateDistance(lat1, lng1, lat2, lng2) {
  // Haversine formula
  const R = 6371000; // Earth's radius in meters
  const toRad = (deg) => deg * Math.PI / 180;

  const dLat = toRad(lat2 - lat1);
  const dLng = toRad(lng2 - lng1);
  const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
            Math.cos(toRad(lat1)) * Math.cos(toRad(lat2)) *
            Math.sin(dLng/2) * Math.sin(dLng/2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

  return R * c; // Distance in meters
}

function updateDistanceToCampsite(userLat, userLng, campsite) {
  const distance = calculateDistance(
    userLat, userLng,
    campsite.latitude, campsite.longitude
  );

  const distanceStr = distance < 1000
    ? `${Math.round(distance)} m`
    : `${(distance / 1000).toFixed(1)} km`;

  document.getElementById('distance-display').textContent =
    `Distance: ${distanceStr}`;
}

IP-Based Fallback

When geolocation permission is denied or unavailable, fall back to IP-based location:

async function fetchIPLocation() {
  try {
    const response = await fetch('https://ipapi.co/json/');
    const data = await response.json();

    if (data.latitude && data.longitude) {
      map.setView([data.latitude, data.longitude], 8);
      addUserMarker(data.latitude, data.longitude);
      console.log(`IP-based location: ${data.city}, ${data.country_name}`);
    } else {
      // Default center
      map.setView([23.5, 121.0], 8);
    }
  } catch (err) {
    console.log('Could not determine location from IP.');
    map.setView([23.5, 121.0], 8);
  }
}

Permission Handling

// Check if permission has already been decided
const permissionStatus = await navigator.permissions.query({ name: 'geolocation' });

permissionStatus.onchange = function() {
  if (this.state === 'granted') {
    console.log('Permission granted — starting location services.');
    startTracking();
  } else if (this.state === 'denied') {
    console.log('Permission denied — using IP fallback.');
    fetchIPLocation();
  }
};

Best Practices

| Practice | Reason | |----------|--------| | Always handle errors gracefully | Permission denied, timeout, unavailable | | Tell users why you need location | Show a brief explanation before requesting | | Use enableHighAccuracy: trueon mobile | GPS is significantly more accurate | | Cache location withmaximumAge` | Don't hammer the GPS sensor | | Provide a fallback | IP location or default center when GPS is unavailable | | Show accuracy circle | Users see how precise their location is | | Use Haversine formula for distance | Accurate distance calculation for any coordinates | | Request permission on user action (not on page load) | Higher permission acceptance rate |

Summary

Geolocation turns your camping map into a practical tool users can rely on in the field. Get the user's location, center the map, show distance to campsites, and handle errors gracefully. Always provide a fallback when GPS is unavailable.

Key takeaways:

  • getCurrentPosition() for one-time location, watchPosition() for tracking
  • Handle all error cases: denied, unavailable, timeout
  • Request permission on user action (button click), not on page load
  • Show user location with a blue dot and accuracy circle
  • Use the Haversine formula for accurate distance calculation
  • Fall back to IP-based location when GPS is unavailable
  • Cache location with maximumAge to save battery
  • Use enableHighAccuracy: true on mobile devices

What's Next: Marker Clustering

The next chapter covers marker clustering — handling hundreds or thousands of campsite markers with efficient clustering algorithms for a clean map.

Unlock Full Tutorial

This chapter is paid content. Join the project to unlock over 5000 words of deep analysis, including 10+ god-tier Prompts and real Source Code examples!