Browser APIs

JavaScript Geolocation API: Complete Location Services Guide

Master the Geolocation API in JavaScript for location-based features. Learn positioning, tracking, and building location-aware applications.

By JavaScriptDoc Team
geolocationgpslocationjavascriptmaps

JavaScript Geolocation API: Complete Location Services Guide

The Geolocation API provides access to geographical location information, enabling you to build location-aware web applications with features like maps, local search, and location tracking.

Understanding Geolocation

The Geolocation API allows web applications to access the user's geographical position with their permission.

// Check if Geolocation is supported
if ('geolocation' in navigator) {
  console.log('Geolocation is supported');
} else {
  console.log('Geolocation is not supported');
}

// Basic position request
navigator.geolocation.getCurrentPosition(
  (position) => {
    console.log('Latitude:', position.coords.latitude);
    console.log('Longitude:', position.coords.longitude);
    console.log('Accuracy:', position.coords.accuracy, 'meters');
  },
  (error) => {
    console.error('Error getting location:', error.message);
  }
);

// Position object structure
/*
position = {
  coords: {
    latitude: 37.7749,
    longitude: -122.4194,
    altitude: null,
    accuracy: 20,
    altitudeAccuracy: null,
    heading: null,
    speed: null
  },
  timestamp: 1634567890123
}
*/

Getting Current Position

Advanced Position Options

class LocationService {
  constructor() {
    this.lastPosition = null;
    this.watchId = null;
  }

  // Get current position with options
  getCurrentPosition(options = {}) {
    const defaultOptions = {
      enableHighAccuracy: false,
      timeout: 5000,
      maximumAge: 0,
    };

    const finalOptions = { ...defaultOptions, ...options };

    return new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(
        (position) => {
          this.lastPosition = position;
          resolve(this.formatPosition(position));
        },
        (error) => {
          reject(this.handleError(error));
        },
        finalOptions
      );
    });
  }

  // Format position data
  formatPosition(position) {
    return {
      latitude: position.coords.latitude,
      longitude: position.coords.longitude,
      accuracy: position.coords.accuracy,
      altitude: position.coords.altitude,
      altitudeAccuracy: position.coords.altitudeAccuracy,
      heading: position.coords.heading,
      speed: position.coords.speed,
      timestamp: new Date(position.timestamp),
      // Additional calculated fields
      coordinates: [position.coords.longitude, position.coords.latitude],
      accuracyLevel: this.getAccuracyLevel(position.coords.accuracy),
      isMoving: position.coords.speed > 0,
    };
  }

  // Determine accuracy level
  getAccuracyLevel(accuracy) {
    if (accuracy <= 5) return 'excellent';
    if (accuracy <= 10) return 'good';
    if (accuracy <= 25) return 'moderate';
    if (accuracy <= 50) return 'fair';
    return 'poor';
  }

  // Handle geolocation errors
  handleError(error) {
    const errorMessages = {
      [error.PERMISSION_DENIED]: 'User denied location permission',
      [error.POSITION_UNAVAILABLE]: 'Location information unavailable',
      [error.TIMEOUT]: 'Location request timed out',
    };

    return {
      code: error.code,
      message: errorMessages[error.code] || error.message,
      timestamp: new Date(),
    };
  }

  // Request high accuracy position
  async getHighAccuracyPosition(timeout = 10000) {
    try {
      // First try with high accuracy
      return await this.getCurrentPosition({
        enableHighAccuracy: true,
        timeout: timeout,
        maximumAge: 0,
      });
    } catch (error) {
      console.warn('High accuracy failed, falling back:', error);

      // Fallback to lower accuracy
      return await this.getCurrentPosition({
        enableHighAccuracy: false,
        timeout: timeout / 2,
        maximumAge: 30000,
      });
    }
  }

  // Get position with retry logic
  async getPositionWithRetry(maxRetries = 3, delay = 1000) {
    let lastError;

    for (let i = 0; i < maxRetries; i++) {
      try {
        return await this.getCurrentPosition();
      } catch (error) {
        lastError = error;

        if (error.code === 1) {
          // Permission denied, no point retrying
          throw error;
        }

        if (i < maxRetries - 1) {
          await new Promise((resolve) => setTimeout(resolve, delay));
        }
      }
    }

    throw lastError;
  }
}

// Usage
const locationService = new LocationService();

// Get current position
locationService
  .getCurrentPosition()
  .then((position) => {
    console.log('Current position:', position);
  })
  .catch((error) => {
    console.error('Location error:', error);
  });

// Get high accuracy position
locationService.getHighAccuracyPosition().then((position) => {
  console.log('Accurate position:', position);
});

Watching Position Changes

Continuous Position Tracking

class PositionTracker {
  constructor() {
    this.watchId = null;
    this.positions = [];
    this.callbacks = new Map();
    this.isTracking = false;
  }

  // Start watching position
  startTracking(options = {}) {
    if (this.isTracking) {
      console.warn('Already tracking position');
      return;
    }

    const defaultOptions = {
      enableHighAccuracy: true,
      timeout: 5000,
      maximumAge: 0,
    };

    const finalOptions = { ...defaultOptions, ...options };

    this.watchId = navigator.geolocation.watchPosition(
      (position) => this.handlePositionUpdate(position),
      (error) => this.handleError(error),
      finalOptions
    );

    this.isTracking = true;
  }

  // Stop watching position
  stopTracking() {
    if (this.watchId !== null) {
      navigator.geolocation.clearWatch(this.watchId);
      this.watchId = null;
      this.isTracking = false;
    }
  }

  // Handle position updates
  handlePositionUpdate(position) {
    const formattedPosition = this.formatPosition(position);

    // Add to history
    this.positions.push(formattedPosition);

    // Calculate additional metrics
    if (this.positions.length > 1) {
      const prevPosition = this.positions[this.positions.length - 2];
      formattedPosition.distance = this.calculateDistance(
        prevPosition,
        formattedPosition
      );
      formattedPosition.bearing = this.calculateBearing(
        prevPosition,
        formattedPosition
      );
    }

    // Notify callbacks
    this.notifyCallbacks('update', formattedPosition);
  }

  // Calculate distance between two points
  calculateDistance(pos1, pos2) {
    const R = 6371e3; // Earth's radius in meters
    const φ1 = (pos1.latitude * Math.PI) / 180;
    const φ2 = (pos2.latitude * Math.PI) / 180;
    const Δφ = ((pos2.latitude - pos1.latitude) * Math.PI) / 180;
    const Δλ = ((pos2.longitude - pos1.longitude) * Math.PI) / 180;

    const a =
      Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
      Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);

    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

    return R * c; // Distance in meters
  }

  // Calculate bearing between two points
  calculateBearing(pos1, pos2) {
    const φ1 = (pos1.latitude * Math.PI) / 180;
    const φ2 = (pos2.latitude * Math.PI) / 180;
    const Δλ = ((pos2.longitude - pos1.longitude) * Math.PI) / 180;

    const y = Math.sin(Δλ) * Math.cos(φ2);
    const x =
      Math.cos(φ1) * Math.sin(φ2) - Math.sin(φ1) * Math.cos(φ2) * Math.cos(Δλ);

    const θ = Math.atan2(y, x);

    return ((θ * 180) / Math.PI + 360) % 360; // Bearing in degrees
  }

  // Subscribe to position updates
  subscribe(event, callback) {
    if (!this.callbacks.has(event)) {
      this.callbacks.set(event, []);
    }

    this.callbacks.get(event).push(callback);

    // Return unsubscribe function
    return () => {
      const callbacks = this.callbacks.get(event);
      const index = callbacks.indexOf(callback);
      if (index > -1) {
        callbacks.splice(index, 1);
      }
    };
  }

  // Notify all callbacks
  notifyCallbacks(event, data) {
    const callbacks = this.callbacks.get(event) || [];
    callbacks.forEach((callback) => callback(data));
  }

  // Get movement statistics
  getStatistics() {
    if (this.positions.length < 2) {
      return null;
    }

    let totalDistance = 0;
    let maxSpeed = 0;
    let totalSpeed = 0;
    let speedCount = 0;

    for (let i = 1; i < this.positions.length; i++) {
      const pos1 = this.positions[i - 1];
      const pos2 = this.positions[i];

      const distance = this.calculateDistance(pos1, pos2);
      totalDistance += distance;

      if (pos2.speed !== null) {
        maxSpeed = Math.max(maxSpeed, pos2.speed);
        totalSpeed += pos2.speed;
        speedCount++;
      }
    }

    const duration =
      this.positions[this.positions.length - 1].timestamp -
      this.positions[0].timestamp;

    return {
      totalDistance,
      duration,
      averageSpeed: speedCount > 0 ? totalSpeed / speedCount : 0,
      maxSpeed,
      pointCount: this.positions.length,
    };
  }

  formatPosition(position) {
    return {
      latitude: position.coords.latitude,
      longitude: position.coords.longitude,
      accuracy: position.coords.accuracy,
      speed: position.coords.speed,
      timestamp: new Date(position.timestamp),
    };
  }

  handleError(error) {
    this.notifyCallbacks('error', error);
  }
}

// Usage
const tracker = new PositionTracker();

// Subscribe to updates
const unsubscribe = tracker.subscribe('update', (position) => {
  console.log('Position update:', position);

  if (position.distance) {
    console.log(`Moved ${position.distance.toFixed(2)} meters`);
  }
});

// Start tracking
tracker.startTracking({
  enableHighAccuracy: true,
  maximumAge: 0,
});

// Get statistics after some time
setTimeout(() => {
  const stats = tracker.getStatistics();
  console.log('Movement statistics:', stats);
}, 30000);

Geofencing

Location-Based Triggers

class Geofence {
  constructor(id, latitude, longitude, radius, options = {}) {
    this.id = id;
    this.center = { latitude, longitude };
    this.radius = radius; // in meters
    this.options = {
      enterCallback: null,
      exitCallback: null,
      dwellCallback: null,
      dwellTime: 30000, // 30 seconds
      ...options,
    };

    this.isInside = false;
    this.enterTime = null;
    this.dwellTimer = null;
  }

  // Check if position is inside geofence
  contains(position) {
    const distance = this.calculateDistance(this.center, {
      latitude: position.latitude,
      longitude: position.longitude,
    });

    return distance <= this.radius;
  }

  // Update geofence state with new position
  update(position) {
    const wasInside = this.isInside;
    this.isInside = this.contains(position);

    if (!wasInside && this.isInside) {
      // Entered geofence
      this.handleEnter(position);
    } else if (wasInside && !this.isInside) {
      // Exited geofence
      this.handleExit(position);
    }
  }

  handleEnter(position) {
    this.enterTime = Date.now();

    if (this.options.enterCallback) {
      this.options.enterCallback({
        geofenceId: this.id,
        position,
        timestamp: new Date(),
      });
    }

    // Start dwell timer
    if (this.options.dwellCallback) {
      this.dwellTimer = setTimeout(() => {
        this.options.dwellCallback({
          geofenceId: this.id,
          position,
          dwellTime: this.options.dwellTime,
          timestamp: new Date(),
        });
      }, this.options.dwellTime);
    }
  }

  handleExit(position) {
    const duration = this.enterTime ? Date.now() - this.enterTime : 0;

    if (this.options.exitCallback) {
      this.options.exitCallback({
        geofenceId: this.id,
        position,
        duration,
        timestamp: new Date(),
      });
    }

    // Clear dwell timer
    if (this.dwellTimer) {
      clearTimeout(this.dwellTimer);
      this.dwellTimer = null;
    }

    this.enterTime = null;
  }

  calculateDistance(pos1, pos2) {
    const R = 6371e3;
    const φ1 = (pos1.latitude * Math.PI) / 180;
    const φ2 = (pos2.latitude * Math.PI) / 180;
    const Δφ = ((pos2.latitude - pos1.latitude) * Math.PI) / 180;
    const Δλ = ((pos2.longitude - pos1.longitude) * Math.PI) / 180;

    const a =
      Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
      Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);

    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

    return R * c;
  }
}

class GeofenceManager {
  constructor() {
    this.geofences = new Map();
    this.tracker = new PositionTracker();
    this.isMonitoring = false;
  }

  // Add a geofence
  addGeofence(geofence) {
    this.geofences.set(geofence.id, geofence);

    // Check current position if available
    if (this.tracker.positions.length > 0) {
      const currentPosition =
        this.tracker.positions[this.tracker.positions.length - 1];
      geofence.update(currentPosition);
    }
  }

  // Remove a geofence
  removeGeofence(id) {
    this.geofences.delete(id);
  }

  // Start monitoring
  startMonitoring(options = {}) {
    if (this.isMonitoring) return;

    this.tracker.subscribe('update', (position) => {
      this.checkGeofences(position);
    });

    this.tracker.startTracking({
      enableHighAccuracy: true,
      ...options,
    });

    this.isMonitoring = true;
  }

  // Stop monitoring
  stopMonitoring() {
    this.tracker.stopTracking();
    this.isMonitoring = false;
  }

  // Check all geofences
  checkGeofences(position) {
    for (const geofence of this.geofences.values()) {
      geofence.update(position);
    }
  }

  // Get nearby geofences
  getNearbyGeofences(position, maxDistance) {
    const nearby = [];

    for (const geofence of this.geofences.values()) {
      const distance = geofence.calculateDistance(geofence.center, {
        latitude: position.latitude,
        longitude: position.longitude,
      });

      if (distance <= maxDistance) {
        nearby.push({
          geofence,
          distance,
          isInside: geofence.contains(position),
        });
      }
    }

    return nearby.sort((a, b) => a.distance - b.distance);
  }
}

// Usage
const geofenceManager = new GeofenceManager();

// Create a geofence for a store
const storeGeofence = new Geofence(
  'store-123',
  37.7749, // latitude
  -122.4194, // longitude
  100, // radius in meters
  {
    enterCallback: (event) => {
      console.log('Entered store area:', event);
      // Send notification
    },
    exitCallback: (event) => {
      console.log('Left store area:', event);
      console.log('Duration:', event.duration / 1000, 'seconds');
    },
    dwellCallback: (event) => {
      console.log('Dwelling in store area:', event);
      // Offer special promotion
    },
  }
);

geofenceManager.addGeofence(storeGeofence);
geofenceManager.startMonitoring();

Location Utilities

Geocoding and Reverse Geocoding

class LocationUtilities {
  constructor(apiKey = null) {
    this.apiKey = apiKey;
  }

  // Convert address to coordinates (using Nominatim)
  async geocode(address) {
    const url =
      `https://nominatim.openstreetmap.org/search?` +
      `q=${encodeURIComponent(address)}&format=json&limit=1`;

    try {
      const response = await fetch(url, {
        headers: {
          'User-Agent': 'MyApp/1.0',
        },
      });

      const data = await response.json();

      if (data.length > 0) {
        return {
          latitude: parseFloat(data[0].lat),
          longitude: parseFloat(data[0].lon),
          displayName: data[0].display_name,
          boundingBox: data[0].boundingbox.map(parseFloat),
        };
      }

      throw new Error('No results found');
    } catch (error) {
      console.error('Geocoding error:', error);
      throw error;
    }
  }

  // Convert coordinates to address
  async reverseGeocode(latitude, longitude) {
    const url =
      `https://nominatim.openstreetmap.org/reverse?` +
      `lat=${latitude}&lon=${longitude}&format=json`;

    try {
      const response = await fetch(url, {
        headers: {
          'User-Agent': 'MyApp/1.0',
        },
      });

      const data = await response.json();

      return {
        displayName: data.display_name,
        address: data.address,
        placeId: data.place_id,
        osmType: data.osm_type,
        osmId: data.osm_id,
      };
    } catch (error) {
      console.error('Reverse geocoding error:', error);
      throw error;
    }
  }

  // Format coordinates for display
  formatCoordinates(latitude, longitude, format = 'decimal') {
    switch (format) {
      case 'decimal':
        return `${latitude.toFixed(6)}, ${longitude.toFixed(6)}`;

      case 'dms':
        return `${this.toDMS(latitude, 'lat')} ${this.toDMS(longitude, 'lon')}`;

      case 'url':
        return `https://www.google.com/maps?q=${latitude},${longitude}`;

      default:
        return { latitude, longitude };
    }
  }

  // Convert decimal to degrees, minutes, seconds
  toDMS(decimal, type) {
    const absolute = Math.abs(decimal);
    const degrees = Math.floor(absolute);
    const minutesDecimal = (absolute - degrees) * 60;
    const minutes = Math.floor(minutesDecimal);
    const seconds = ((minutesDecimal - minutes) * 60).toFixed(1);

    const direction =
      type === 'lat' ? (decimal >= 0 ? 'N' : 'S') : decimal >= 0 ? 'E' : 'W';

    return `${degrees}°${minutes}'${seconds}"${direction}`;
  }

  // Calculate bounds for a set of coordinates
  calculateBounds(coordinates) {
    if (coordinates.length === 0) return null;

    let minLat = Infinity,
      maxLat = -Infinity;
    let minLng = Infinity,
      maxLng = -Infinity;

    for (const coord of coordinates) {
      minLat = Math.min(minLat, coord.latitude);
      maxLat = Math.max(maxLat, coord.latitude);
      minLng = Math.min(minLng, coord.longitude);
      maxLng = Math.max(maxLng, coord.longitude);
    }

    return {
      southwest: { latitude: minLat, longitude: minLng },
      northeast: { latitude: maxLat, longitude: maxLng },
      center: {
        latitude: (minLat + maxLat) / 2,
        longitude: (minLng + maxLng) / 2,
      },
    };
  }

  // Find nearest location from a list
  findNearest(position, locations) {
    let nearest = null;
    let minDistance = Infinity;

    for (const location of locations) {
      const distance = this.calculateDistance(position, location);

      if (distance < minDistance) {
        minDistance = distance;
        nearest = location;
      }
    }

    return {
      location: nearest,
      distance: minDistance,
    };
  }

  calculateDistance(pos1, pos2) {
    const R = 6371e3;
    const φ1 = (pos1.latitude * Math.PI) / 180;
    const φ2 = (pos2.latitude * Math.PI) / 180;
    const Δφ = ((pos2.latitude - pos1.latitude) * Math.PI) / 180;
    const Δλ = ((pos2.longitude - pos1.longitude) * Math.PI) / 180;

    const a =
      Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
      Math.cos(φ1) * Math.cos(φ2) * Math.sin(Δλ / 2) * Math.sin(Δλ / 2);

    const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

    return R * c;
  }
}

// Usage
const utils = new LocationUtilities();

// Geocode an address
utils.geocode('1600 Amphitheatre Parkway, Mountain View, CA').then((result) => {
  console.log('Coordinates:', result);
});

// Reverse geocode
navigator.geolocation.getCurrentPosition(async (position) => {
  const address = await utils.reverseGeocode(
    position.coords.latitude,
    position.coords.longitude
  );
  console.log('Current address:', address);
});

Location Privacy

Privacy-Aware Location Handling

class PrivacyAwareLocation {
  constructor() {
    this.permissionState = 'prompt';
    this.privacyLevel = 'precise'; // precise, approximate, city
  }

  // Check permission state
  async checkPermission() {
    if ('permissions' in navigator) {
      try {
        const result = await navigator.permissions.query({
          name: 'geolocation',
        });
        this.permissionState = result.state;

        // Listen for permission changes
        result.addEventListener('change', () => {
          this.permissionState = result.state;
          this.onPermissionChange(result.state);
        });

        return result.state;
      } catch (error) {
        console.error('Permission check failed:', error);
      }
    }

    return 'unknown';
  }

  // Request location with privacy options
  async requestLocation(privacyLevel = 'precise') {
    this.privacyLevel = privacyLevel;

    // Check permission first
    const permission = await this.checkPermission();

    if (permission === 'denied') {
      throw new Error('Location permission denied');
    }

    // Get location based on privacy level
    switch (privacyLevel) {
      case 'precise':
        return this.getPreciseLocation();

      case 'approximate':
        return this.getApproximateLocation();

      case 'city':
        return this.getCityLocation();

      default:
        throw new Error('Invalid privacy level');
    }
  }

  // Get precise location
  async getPreciseLocation() {
    return new Promise((resolve, reject) => {
      navigator.geolocation.getCurrentPosition(
        (position) => {
          resolve({
            latitude: position.coords.latitude,
            longitude: position.coords.longitude,
            accuracy: position.coords.accuracy,
            privacyLevel: 'precise',
          });
        },
        reject,
        {
          enableHighAccuracy: true,
          timeout: 10000,
          maximumAge: 0,
        }
      );
    });
  }

  // Get approximate location (reduced precision)
  async getApproximateLocation() {
    const precise = await this.getPreciseLocation();

    // Round to ~1km precision
    return {
      latitude: Math.round(precise.latitude * 100) / 100,
      longitude: Math.round(precise.longitude * 100) / 100,
      accuracy: 1000, // 1km
      privacyLevel: 'approximate',
    };
  }

  // Get city-level location
  async getCityLocation() {
    const precise = await this.getPreciseLocation();

    // Use reverse geocoding to get city
    const utils = new LocationUtilities();
    const address = await utils.reverseGeocode(
      precise.latitude,
      precise.longitude
    );

    // Return city center coordinates
    if (address.address.city) {
      const cityCoords = await utils.geocode(
        `${address.address.city}, ${address.address.country}`
      );

      return {
        latitude: cityCoords.latitude,
        longitude: cityCoords.longitude,
        accuracy: 10000, // 10km
        city: address.address.city,
        country: address.address.country,
        privacyLevel: 'city',
      };
    }

    // Fallback to very approximate location
    return this.getApproximateLocation();
  }

  // Store location with privacy considerations
  storeLocation(location, options = {}) {
    const {
      encrypt = false,
      expiry = 3600000, // 1 hour
    } = options;

    const data = {
      location,
      timestamp: Date.now(),
      expiry: Date.now() + expiry,
    };

    if (encrypt) {
      // Simple obfuscation (use proper encryption in production)
      const encrypted = btoa(JSON.stringify(data));
      localStorage.setItem('user_location', encrypted);
    } else {
      localStorage.setItem('user_location', JSON.stringify(data));
    }
  }

  // Retrieve stored location
  getStoredLocation(decrypt = false) {
    const stored = localStorage.getItem('user_location');
    if (!stored) return null;

    try {
      const data = decrypt ? JSON.parse(atob(stored)) : JSON.parse(stored);

      // Check expiry
      if (data.expiry && Date.now() > data.expiry) {
        localStorage.removeItem('user_location');
        return null;
      }

      return data.location;
    } catch (error) {
      console.error('Failed to retrieve stored location:', error);
      return null;
    }
  }

  // Clear location data
  clearLocationData() {
    localStorage.removeItem('user_location');
  }

  onPermissionChange(state) {
    console.log('Location permission changed:', state);

    if (state === 'denied') {
      this.clearLocationData();
    }
  }
}

// Usage
const privacyLocation = new PrivacyAwareLocation();

// Check permission
privacyLocation.checkPermission().then((state) => {
  console.log('Permission state:', state);
});

// Request with privacy options
async function getUserLocation() {
  try {
    // Try precise first
    const location = await privacyLocation.requestLocation('precise');
    console.log('Precise location:', location);
  } catch (error) {
    // Fall back to approximate
    try {
      const location = await privacyLocation.requestLocation('approximate');
      console.log('Approximate location:', location);
    } catch (error) {
      console.error('Location request failed:', error);
    }
  }
}

Map Integration

Working with Map Services

class MapIntegration {
  constructor(mapContainer) {
    this.mapContainer = mapContainer;
    this.markers = new Map();
    this.userLocation = null;
  }

  // Create a simple map visualization
  createMap(center, zoom = 13) {
    // Create map iframe (using OpenStreetMap)
    const iframe = document.createElement('iframe');
    iframe.width = '100%';
    iframe.height = '400';
    iframe.frameBorder = '0';
    iframe.scrolling = 'no';
    iframe.marginHeight = '0';
    iframe.marginWidth = '0';

    const bbox = this.calculateBoundingBox(center, zoom);
    iframe.src =
      `https://www.openstreetmap.org/export/embed.html?` +
      `bbox=${bbox.join(',')}&layer=mapnik`;

    this.mapContainer.appendChild(iframe);

    return iframe;
  }

  // Calculate bounding box for zoom level
  calculateBoundingBox(center, zoom) {
    const scale =
      (156543.03392 * Math.cos((center.latitude * Math.PI) / 180)) /
      Math.pow(2, zoom);
    const halfWidth = scale * 400;
    const halfHeight = scale * 300;

    const metersPerDegLat = 111320;
    const metersPerDegLon =
      metersPerDegLat * Math.cos((center.latitude * Math.PI) / 180);

    return [
      center.longitude - halfWidth / metersPerDegLon,
      center.latitude - halfHeight / metersPerDegLat,
      center.longitude + halfWidth / metersPerDegLon,
      center.latitude + halfHeight / metersPerDegLat,
    ];
  }

  // Generate static map URL
  generateStaticMapUrl(center, options = {}) {
    const { zoom = 13, width = 600, height = 400, markers = [] } = options;

    // Using MapQuest static API (requires API key)
    const baseUrl = 'https://www.mapquestapi.com/staticmap/v5/map';
    const params = new URLSearchParams({
      key: 'YOUR_API_KEY',
      center: `${center.latitude},${center.longitude}`,
      zoom: zoom,
      size: `${width},${height}`,
      type: 'map',
    });

    // Add markers
    markers.forEach((marker, index) => {
      params.append(
        'locations',
        `${marker.latitude},${marker.longitude}|marker-${index + 1}`
      );
    });

    return `${baseUrl}?${params.toString()}`;
  }

  // Show user location on map
  async showUserLocation() {
    try {
      const locationService = new LocationService();
      const position = await locationService.getCurrentPosition();

      this.userLocation = position;

      // Create or update map
      if (!this.map) {
        this.map = this.createMap({
          latitude: position.latitude,
          longitude: position.longitude,
        });
      }

      // Add user marker (visual indicator)
      this.addUserLocationIndicator(position);

      return position;
    } catch (error) {
      console.error('Failed to show user location:', error);
      throw error;
    }
  }

  // Add visual indicator for user location
  addUserLocationIndicator(position) {
    const indicator = document.createElement('div');
    indicator.className = 'user-location-indicator';
    indicator.style.cssText = `
      position: absolute;
      top: 50%;
      left: 50%;
      transform: translate(-50%, -50%);
      width: 20px;
      height: 20px;
      background: #4285f4;
      border: 3px solid white;
      border-radius: 50%;
      box-shadow: 0 2px 4px rgba(0,0,0,0.3);
      z-index: 1000;
    `;

    // Add pulsing animation
    const style = document.createElement('style');
    style.textContent = `
      @keyframes pulse {
        0% { box-shadow: 0 0 0 0 rgba(66, 133, 244, 0.7); }
        70% { box-shadow: 0 0 0 20px rgba(66, 133, 244, 0); }
        100% { box-shadow: 0 0 0 0 rgba(66, 133, 244, 0); }
      }
      .user-location-indicator {
        animation: pulse 2s infinite;
      }
    `;

    document.head.appendChild(style);
    this.mapContainer.style.position = 'relative';
    this.mapContainer.appendChild(indicator);
  }

  // Calculate route between points
  async calculateRoute(start, end) {
    // Using OpenRouteService (requires API key)
    const url = 'https://api.openrouteservice.org/v2/directions/driving-car';

    const response = await fetch(url, {
      method: 'POST',
      headers: {
        Authorization: 'YOUR_API_KEY',
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({
        coordinates: [
          [start.longitude, start.latitude],
          [end.longitude, end.latitude],
        ],
      }),
    });

    const data = await response.json();

    if (data.routes && data.routes.length > 0) {
      const route = data.routes[0];
      return {
        distance: route.summary.distance,
        duration: route.summary.duration,
        geometry: route.geometry,
      };
    }

    throw new Error('No route found');
  }
}

Best Practices

  1. Always handle errors gracefully

    navigator.geolocation.getCurrentPosition(success, (error) => {
      // Provide fallback or user guidance
      showLocationError(error);
    });
    
  2. Respect user privacy

    // Ask for permission context
    showLocationPermissionDialog({
      title: 'Enable Location',
      message: 'We need your location to show nearby stores',
    });
    
  3. Use appropriate accuracy

    // Don't use high accuracy unless needed
    const options = {
      enableHighAccuracy: false, // Better battery life
      maximumAge: 300000, // 5 minutes cache
    };
    
  4. Clean up watchers

    // Always clear watch when done
    const watchId = navigator.geolocation.watchPosition(success);
    // Later...
    navigator.geolocation.clearWatch(watchId);
    

Conclusion

The Geolocation API enables powerful location features:

  • Position tracking for real-time updates
  • Geofencing for location-based triggers
  • Privacy controls for user trust
  • Map integration for visualization
  • Location utilities for geocoding
  • Cross-platform support

Key takeaways:

  • Always handle permissions properly
  • Provide privacy options to users
  • Use appropriate accuracy levels
  • Handle errors gracefully
  • Test on real devices
  • Consider battery impact

Master the Geolocation API to build engaging location-aware applications!