javascript-today

Geolocation API

What is the Geolocation API?

The Geolocation API allows you to access the user’s geographic location (with their permission). Perfect for maps, location-based services, and local recommendations.

if ('geolocation' in navigator) {
  navigator.geolocation.getCurrentPosition((position) => {
    console.log('Latitude:', position.coords.latitude);
    console.log('Longitude:', position.coords.longitude);
  });
}

Important: Requires HTTPS (except localhost) and user permission.

Checking for Support

function supportsGeolocation() {
  return 'geolocation' in navigator;
}

if (supportsGeolocation()) {
  console.log('Geolocation is supported');
} else {
  console.log('Geolocation is not supported');
  // Show fallback UI
}

Getting Current Position

Basic Usage

function getLocation() {
  if ('geolocation' in navigator) {
    navigator.geolocation.getCurrentPosition(
      // Success callback
      (position) => {
        const lat = position.coords.latitude;
        const lon = position.coords.longitude;
        const accuracy = position.coords.accuracy;
        
        console.log(`Location: ${lat}, ${lon}`);
        console.log(`Accuracy: ${accuracy} meters`);
      },
      // Error callback
      (error) => {
        console.error('Error getting location:', error.message);
      }
    );
  } else {
    console.error('Geolocation not supported');
  }
}

getLocation();

Position Object

navigator.geolocation.getCurrentPosition((position) => {
  const coords = position.coords;
  
  console.log('Latitude:', coords.latitude);        // Decimal degrees
  console.log('Longitude:', coords.longitude);      // Decimal degrees
  console.log('Accuracy:', coords.accuracy);        // Meters
  console.log('Altitude:', coords.altitude);        // Meters (may be null)
  console.log('Altitude Accuracy:', coords.altitudeAccuracy);  // Meters (may be null)
  console.log('Heading:', coords.heading);          // Degrees (may be null)
  console.log('Speed:', coords.speed);              // Meters/second (may be null)
  console.log('Timestamp:', position.timestamp);    // When position was acquired
});

Handling Errors

function handleLocationError(error) {
  switch (error.code) {
    case error.PERMISSION_DENIED:
      console.error('User denied geolocation permission');
      showMessage('Please enable location access');
      break;
      
    case error.POSITION_UNAVAILABLE:
      console.error('Location information unavailable');
      showMessage('Unable to determine your location');
      break;
      
    case error.TIMEOUT:
      console.error('Location request timed out');
      showMessage('Location request took too long');
      break;
      
    default:
      console.error('Unknown error:', error.message);
      showMessage('An error occurred');
  }
}

navigator.geolocation.getCurrentPosition(
  successCallback,
  handleLocationError
);

Position Options

Customize how location is obtained:

const options = {
  enableHighAccuracy: true,  // Request best possible accuracy
  timeout: 10000,            // Wait max 10 seconds
  maximumAge: 0              // Don't use cached position
};

navigator.geolocation.getCurrentPosition(
  (position) => {
    console.log('Got position:', position.coords);
  },
  (error) => {
    console.error('Error:', error);
  },
  options
);

Option Details

// High accuracy (uses GPS, slower, more battery)
const highAccuracy = {
  enableHighAccuracy: true,
  timeout: 15000,
  maximumAge: 0
};

// Fast, less accurate (uses WiFi/IP, faster, less battery)
const lowAccuracy = {
  enableHighAccuracy: false,
  timeout: 5000,
  maximumAge: 30000  // Accept position up to 30 seconds old
};

// Use appropriate option based on needs
navigator.geolocation.getCurrentPosition(success, error, highAccuracy);

Watching Position (Live Tracking)

Track user movement in real-time:

let watchId;

function startTracking() {
  if ('geolocation' in navigator) {
    watchId = navigator.geolocation.watchPosition(
      (position) => {
        const lat = position.coords.latitude;
        const lon = position.coords.longitude;
        const speed = position.coords.speed;
        
        console.log(`Moving: ${lat}, ${lon}`);
        if (speed) {
          console.log(`Speed: ${speed} m/s`);
        }
        
        updateMapMarker(lat, lon);
      },
      (error) => {
        console.error('Tracking error:', error.message);
      },
      {
        enableHighAccuracy: true,
        timeout: 5000,
        maximumAge: 0
      }
    );
    
    console.log('Started tracking with ID:', watchId);
  }
}

function stopTracking() {
  if (watchId) {
    navigator.geolocation.clearWatch(watchId);
    console.log('Stopped tracking');
    watchId = null;
  }
}

// Start tracking
startTracking();

// Stop tracking when done
// stopTracking();

Practical Examples

1. Show User Location on Map

function showUserOnMap() {
  navigator.geolocation.getCurrentPosition(
    (position) => {
      const lat = position.coords.latitude;
      const lon = position.coords.longitude;
      
      // Using Google Maps
      const mapUrl = `https://www.google.com/maps?q=${lat},${lon}`;
      window.open(mapUrl, '_blank');
      
      // Or display on embedded map
      initMap(lat, lon);
    },
    (error) => {
      alert('Could not get your location: ' + error.message);
    }
  );
}

function initMap(lat, lon) {
  // Example with Google Maps JavaScript API
  const map = new google.maps.Map(document.getElementById('map'), {
    center: { lat, lng: lon },
    zoom: 15
  });
  
  new google.maps.Marker({
    position: { lat, lng: lon },
    map: map,
    title: 'You are here'
  });
}

2. Calculate Distance to Target

function degreesToRadians(degrees) {
  return degrees * (Math.PI / 180);
}

function calculateDistance(lat1, lon1, lat2, lon2) {
  const earthRadiusKm = 6371;
  
  const dLat = degreesToRadians(lat2 - lat1);
  const dLon = degreesToRadians(lon2 - lon1);
  
  const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
            Math.cos(degreesToRadians(lat1)) * Math.cos(degreesToRadians(lat2)) *
            Math.sin(dLon / 2) * Math.sin(dLon / 2);
  
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
  
  return earthRadiusKm * c;
}

function getDistanceToStore() {
  const storeLocation = { lat: 40.7128, lon: -74.0060 };  // New York
  
  navigator.geolocation.getCurrentPosition((position) => {
    const userLat = position.coords.latitude;
    const userLon = position.coords.longitude;
    
    const distance = calculateDistance(
      userLat, userLon,
      storeLocation.lat, storeLocation.lon
    );
    
    console.log(`Store is ${distance.toFixed(2)} km away`);
  });
}

3. Find Nearest Locations

async function findNearestStores() {
  return new Promise((resolve, reject) => {
    navigator.geolocation.getCurrentPosition(
      (position) => {
        const userLat = position.coords.latitude;
        const userLon = position.coords.longitude;
        
        const stores = [
          { name: 'Store A', lat: 40.7128, lon: -74.0060 },
          { name: 'Store B', lat: 40.7589, lon: -73.9851 },
          { name: 'Store C', lat: 40.7306, lon: -73.9352 }
        ];
        
        // Calculate distance to each store
        const storesWithDistance = stores.map(store => ({
          ...store,
          distance: calculateDistance(userLat, userLon, store.lat, store.lon)
        }));
        
        // Sort by distance
        storesWithDistance.sort((a, b) => a.distance - b.distance);
        
        resolve(storesWithDistance);
      },
      reject
    );
  });
}

// Usage
const nearestStores = await findNearestStores();
console.log('Nearest store:', nearestStores[0].name);

4. Location-Based Recommendations

class LocationService {
  constructor() {
    this.currentPosition = null;
  }
  
  async getCurrentLocation() {
    return new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(
        (position) => {
          this.currentPosition = {
            lat: position.coords.latitude,
            lon: position.coords.longitude,
            accuracy: position.coords.accuracy
          };
          resolve(this.currentPosition);
        },
        reject,
        {
          enableHighAccuracy: true,
          timeout: 10000,
          maximumAge: 300000  // 5 minutes
        }
      );
    });
  }
  
  async getNearbyPlaces(radius = 5) {
    const location = await this.getCurrentLocation();
    
    // Call external API (e.g., Google Places API)
    const response = await fetch(
      `https://api.example.com/places?lat=${location.lat}&lon=${location.lon}&radius=${radius}`
    );
    
    return await response.json();
  }
  
  isWithinRadius(targetLat, targetLon, radiusKm) {
    if (!this.currentPosition) {
      throw new Error('Current position not available');
    }
    
    const distance = calculateDistance(
      this.currentPosition.lat,
      this.currentPosition.lon,
      targetLat,
      targetLon
    );
    
    return distance <= radiusKm;
  }
}

// Usage
const locationService = new LocationService();
const places = await locationService.getNearbyPlaces(10);  // 10km radius

5. Geofencing (Track Entry/Exit)

class Geofence {
  constructor(centerLat, centerLon, radiusKm) {
    this.center = { lat: centerLat, lon: centerLon };
    this.radius = radiusKm;
    this.watchId = null;
    this.inside = false;
  }
  
  start(onEnter, onExit) {
    this.watchId = navigator.geolocation.watchPosition(
      (position) => {
        const lat = position.coords.latitude;
        const lon = position.coords.longitude;
        
        const distance = calculateDistance(
          lat, lon,
          this.center.lat, this.center.lon
        );
        
        const currentlyInside = distance <= this.radius;
        
        // Detect crossing boundary
        if (currentlyInside && !this.inside) {
          onEnter();
          this.inside = true;
        } else if (!currentlyInside && this.inside) {
          onExit();
          this.inside = false;
        }
      },
      (error) => {
        console.error('Geofence error:', error);
      },
      {
        enableHighAccuracy: true,
        maximumAge: 0
      }
    );
  }
  
  stop() {
    if (this.watchId) {
      navigator.geolocation.clearWatch(this.watchId);
      this.watchId = null;
    }
  }
}

// Usage
const storeFence = new Geofence(40.7128, -74.0060, 0.5);  // 500m radius

storeFence.start(
  () => console.log('Welcome to our store!'),
  () => console.log('Thanks for visiting!')
);

Permission Handling

Request Permission

async function requestLocationPermission() {
  try {
    const result = await navigator.permissions.query({ name: 'geolocation' });
    
    console.log('Permission state:', result.state);
    // 'granted', 'denied', or 'prompt'
    
    // Listen for permission changes
    result.addEventListener('change', () => {
      console.log('Permission changed to:', result.state);
    });
    
    return result.state;
  } catch (error) {
    console.error('Permission API not supported');
    return 'unknown';
  }
}

// Usage
const permission = await requestLocationPermission();

if (permission === 'granted') {
  getLocation();
} else if (permission === 'denied') {
  showPermissionInstructions();
}

User-Friendly Permission Flow

async function requestLocationAccess() {
  // Check if already granted
  const state = await requestLocationPermission();
  
  if (state === 'granted') {
    return getLocation();
  }
  
  if (state === 'denied') {
    showMessage('Location access denied. Please enable in browser settings.');
    return;
  }
  
  // Show why we need permission
  showModal({
    title: 'Enable Location',
    message: 'We use your location to find nearby stores and show personalized recommendations.',
    onAccept: () => {
      getLocation();
    }
  });
}

Performance Considerations

// Cache position to avoid repeated API calls
class LocationCache {
  constructor(maxAgeMs = 300000) {  // 5 minutes default
    this.maxAge = maxAgeMs;
    this.cachedPosition = null;
    this.cachedTime = null;
  }
  
  async getPosition(options = {}) {
    const now = Date.now();
    
    // Return cached if still valid
    if (this.cachedPosition && this.cachedTime && 
        (now - this.cachedTime) < this.maxAge) {
      return this.cachedPosition;
    }
    
    // Get fresh position
    return new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(
        (position) => {
          this.cachedPosition = position;
          this.cachedTime = now;
          resolve(position);
        },
        reject,
        options
      );
    });
  }
  
  clearCache() {
    this.cachedPosition = null;
    this.cachedTime = null;
  }
}

// Usage
const locationCache = new LocationCache(300000);  // 5 min cache

const position1 = await locationCache.getPosition();  // API call
const position2 = await locationCache.getPosition();  // From cache

Best Practices

DO:

// Always check for support
if ('geolocation' in navigator) {
  // Use geolocation
}

// Provide error handling
navigator.geolocation.getCurrentPosition(success, handleError);

// Explain why you need location
showMessage('We need your location to show nearby stores');

// Use appropriate accuracy
const options = {
  enableHighAccuracy: false  // Use true only if really needed
};

// Clear watch when done
navigator.geolocation.clearWatch(watchId);

// Cache results when appropriate
const cachedLocation = new LocationCache();

DON’T:

// Don't use without HTTPS (except localhost)
// http://example.com  ❌

// Don't forget error handling
navigator.geolocation.getCurrentPosition(success);  // ❌ No error handler

// Don't request location without explanation
getCurrentPosition();  // ❌ User doesn't know why

// Don't use high accuracy unnecessarily
enableHighAccuracy: true  // ❌ Drains battery if not needed

// Don't forget to stop watching
watchPosition();  // ❌ Never stopped, drains battery

// Don't store location without consent
localStorage.setItem('location', coords);  // ❌ Privacy issue

Summary

Get Current Position:

navigator.geolocation.getCurrentPosition(
  (position) => {
    console.log(position.coords.latitude, position.coords.longitude);
  },
  (error) => {
    console.error(error.message);
  },
  { enableHighAccuracy: true, timeout: 10000 }
);

Watch Position:

const watchId = navigator.geolocation.watchPosition(callback);
navigator.geolocation.clearWatch(watchId);  // Stop watching

Key Properties:

  • position.coords.latitude - Decimal degrees
  • position.coords.longitude - Decimal degrees
  • position.coords.accuracy - Accuracy in meters
  • position.coords.speed - Speed in m/s (may be null)

Error Codes:

  • PERMISSION_DENIED (1) - User denied access
  • POSITION_UNAVAILABLE (2) - Can’t determine location
  • TIMEOUT (3) - Request took too long

Requirements:

  • HTTPS (or localhost)
  • User permission
  • Hardware with GPS/location capability