地理位置 API

深入理解 Geolocation API 的完整功能、权限管理、位置精度选项,以及在地图应用和位置感知服务中的应用。

地理位置 API(Geolocation API)

一、Geolocation API 概述

1.1 简介

Geolocation API 允许 Web 应用访问用户的地理位置信息。这个 API 通过 GPS、Wi-Fi、IP 地址等多种方式来确定用户位置。

// 检测支持
if ('geolocation' in navigator) {
  console.log('Geolocation supported');
}

1.2 核心方法

// 获取当前位置
navigator.geolocation.getCurrentPosition(
  successCallback,
  errorCallback,
  options
);

// 持续监听位置变化
navigator.geolocation.watchPosition(
  successCallback,
  errorCallback,
  options
);

// 停止监听
navigator.geolocation.clearWatch(watchId);

二、获取当前位置

2.1 基本用法

function showPosition(position) {
  const lat = position.coords.latitude;
  const lon = position.coords.longitude;
  console.log(`Latitude: ${lat}, Longitude: ${lon}`);
}

function showError(error) {
  switch (error.code) {
    case error.PERMISSION_DENIED:
      console.error('User denied the request for Geolocation');
      break;
    case error.POSITION_UNAVAILABLE:
      console.error('Position information is unavailable');
      break;
    case error.TIMEOUT:
      console.error('The request to get user position timed out');
      break;
    case error.UNKNOWN_ERROR:
      console.error('An unknown error occurred');
      break;
  }
}

navigator.geolocation.getCurrentPosition(showPosition, showError);

2.2 位置信息详解

function showPosition(position) {
  const coords = position.coords;

  // 纬度
  console.log('Latitude:', coords.latitude);

  // 经度
  console.log('Longitude:', coords.longitude);

  // 精度(米)
  console.log('Accuracy:', coords.accuracy);

  // 海拔(米)
  console.log('Altitude:', coords.altitude);

  // 海拔精度(米)
  console.log('Altitude Accuracy:', coords.altitudeAccuracy);

  // 方向(度,0-360)
  console.log('Heading:', coords.heading);

  // 速度(米/秒)
  console.log('Speed:', coords.speed);

  // 时间戳
  console.log('Timestamp:', new Date(position.timestamp));
}

三、选项配置

3.1 位置选项

const options = {
  // 是否使用高精度位置(消耗更多资源)
  enableHighAccuracy: true,

  // 超时时间(毫秒)
  timeout: 10000,

  // 缓存有效期(毫秒)
  // 0 表示每次都获取新位置
  // Infinity 表示使用缓存
  maximumAge: 0
};

navigator.geolocation.getCurrentPosition(
  (position) => console.log(position),
  (error) => console.error(error),
  options
);

3.2 高精度模式

// 高精度模式(通常使用 GPS)
const highAccuracyOptions = {
  enableHighAccuracy: true,
  timeout: 15000,
  maximumAge: 0
};

// 低精度模式(使用 IP/Wi-Fi,更快但不准)
const lowAccuracyOptions = {
  enableHighAccuracy: false,
  timeout: 5000,
  maximumAge: 60000
};

四、持续监听位置

4.1 watchPosition

let watchId;

function startTracking() {
  watchId = navigator.geolocation.watchPosition(
    (position) => {
      console.log('Position updated:', position.coords.latitude, position.coords.longitude);
    },
    (error) => {
      console.error('Tracking error:', error);
    },
    {
      enableHighAccuracy: true,
      timeout: 10000,
      maximumAge: 5000
    }
  );
}

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

// 页面卸载时停止
window.addEventListener('beforeunload', stopTracking);

4.2 实际应用:实时位置追踪

class LocationTracker {
  constructor() {
    this.watchId = null;
    this.positions = [];
    this.onPositionUpdate = null;
  }

  start() {
    if (!('geolocation' in navigator)) {
      console.error('Geolocation not supported');
      return;
    }

    this.watchId = navigator.geolocation.watchPosition(
      (position) => this.handlePosition(position),
      (error) => this.handleError(error),
      {
        enableHighAccuracy: true,
        timeout: 10000,
        maximumAge: 0
      }
    );
  }

  handlePosition(position) {
    const point = {
      lat: position.coords.latitude,
      lon: position.coords.longitude,
      accuracy: position.coords.accuracy,
      timestamp: position.timestamp
    };

    this.positions.push(point);

    if (this.onPositionUpdate) {
      this.onPositionUpdate(point);
    }
  }

  handleError(error) {
    switch (error.code) {
      case error.PERMISSION_DENIED:
        console.error('Permission denied');
        break;
      case error.POSITION_UNAVAILABLE:
        console.error('Position unavailable');
        break;
      case error.TIMEOUT:
        console.error('Position timeout');
        break;
    }
  }

  stop() {
    if (this.watchId) {
      navigator.geolocation.clearWatch(this.watchId);
      this.watchId = null;
    }
  }

  getPath() {
    return this.positions.map(p => [p.lat, p.lon]);
  }
}

// 使用
const tracker = new LocationTracker();
tracker.onPositionUpdate = (point) => {
  console.log('Moved to:', point.lat, point.lon);
  updateMap(point.lat, point.lon);
};
tracker.start();

五、权限管理

5.1 Permissions API

// 查询权限状态
async function checkGeolocationPermission() {
  const permission = await navigator.permissions.query({ name: 'geolocation' });
  console.log('Permission state:', permission.state);
  // 'granted' - 已授权
  // 'prompt' - 待请求
  // 'denied' - 已拒绝

  permission.addEventListener('change', () => {
    console.log('Permission changed:', permission.state);
  });
}

5.2 权限请求最佳实践

async function requestGeolocation() {
  // 检查是否已拒绝
  const permission = await navigator.permissions.query({ name: 'geolocation' });

  if (permission.state === 'denied') {
    console.error('Geolocation permission denied');
    // 引导用户手动开启权限
    return;
  }

  if (permission.state === 'prompt') {
    // getCurrentPosition 会自动触发权限请求
    return navigator.geolocation.getCurrentPosition(
      (position) => console.log(position),
      (error) => console.error(error)
    );
  }

  // 已授权
  return navigator.geolocation.getCurrentPosition(
    (position) => console.log(position),
    (error) => console.error(error)
  );
}

六、实际应用

6.1 计算距离

// Haversine 公式计算两点间距离
function calculateDistance(lat1, lon1, lat2, lon2) {
  const R = 6371e3; // 地球半径(米)
  const φ1 = lat1 * Math.PI / 180;
  const φ2 = lat2 * Math.PI / 180;
  const Δφ = (lat2 - lat1) * Math.PI / 180;
  const Δλ = (lon2 - lon1) * Math.PI / 180;

  const a = Math.sin(Δφ / 2) * Math.sin(Δφ / 2) +
            Math.cos1) * Math.cos2) *
            Math.sin(Δλ / 2) * Math.sin(Δλ / 2);
  const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));

  return R * c; // 距离(米)
}

// 使用
const distance = calculateDistance(39.9042, 116.4074, 31.2304, 121.4737);
console.log(`Distance: ${distance.toFixed(2)} meters`);

6.2 附近地点查找

class NearbySearch {
  constructor(places) {
    this.places = places;
  }

  findNearby(lat, lon, radiusKm = 5) {
    return this.places
      .map(place => ({
        ...place,
        distance: calculateDistance(lat, lon, place.lat, place.lon)
      }))
      .filter(place => place.distance <= radiusKm * 1000)
      .sort((a, b) => a.distance - b.distance);
  }
}

// 使用
const search = new NearbySearch([
  { name: 'Restaurant A', lat: 39.905, lon: 116.408 },
  { name: 'Restaurant B', lat: 39.906, lon: 116.409 },
  { name: 'Restaurant C', lat: 39.910, lon: 116.415 }
]);

const nearby = search.findNearby(39.9042, 116.4074, 2); // 2km 范围内
console.log(nearby);

七、安全与隐私

7.1 隐私保护

  • 地理位置是敏感信息,必须获得用户明确同意
  • HTTPS 是使用 Geolocation API 的前提条件(localhost 除外)
  • 精确位置需要用户在系统级别授权

7.2 错误处理

function handleGeolocationError(error) {
  const messages = {
    [error.PERMISSION_DENIED]: '位置权限被拒绝。请在浏览器设置中允许位置访问。',
    [error.POSITION_UNAVAILABLE]: '无法获取位置信息。请检查您的网络连接。',
    [error.TIMEOUT]: '获取位置超时。请重试。'
  };

  console.error(messages[error.code] || '未知错误');
  showErrorMessage(messages[error.code] || '获取位置失败');
}

参考资料

延展阅读