Geolocation API

Geolocation API允许Web应用获取用户的地理位置信息。本章将介绍Geolocation API的基本概念和使用方法。

获取位置

基本用法

navigator.geolocation.getCurrentPosition(
    (position) => {
        console.log('纬度:', position.coords.latitude);
        console.log('经度:', position.coords.longitude);
        console.log('精度:', position.coords.accuracy);
        console.log('海拔:', position.coords.altitude);
        console.log('海拔精度:', position.coords.altitudeAccuracy);
        console.log('方向:', position.coords.heading);
        console.log('速度:', position.coords.speed);
        console.log('时间戳:', new Date(position.timestamp));
    },
    (error) => {
        console.error('获取位置失败:', error.message);
        console.error('错误码:', error.code);
    },
    {
        enableHighAccuracy: true,
        timeout: 10000,
        maximumAge: 0
    }
);

错误处理

const errorCodes = {
    1: 'PERMISSION_DENIED - 用户拒绝了位置请求',
    2: 'POSITION_UNAVAILABLE - 位置信息不可用',
    3: 'TIMEOUT - 请求超时'
};

function handlePositionError(error) {
    switch (error.code) {
        case error.PERMISSION_DENIED:
            console.error('用户拒绝了位置请求');
            break;
        case error.POSITION_UNAVAILABLE:
            console.error('位置信息不可用');
            break;
        case error.TIMEOUT:
            console.error('请求超时');
            break;
        default:
            console.error('未知错误');
    }
}

位置选项

const positionOptions = {
    enableHighAccuracy: true,
    timeout: 5000,
    maximumAge: 60000
};

const optionsDescription = {
    enableHighAccuracy: {
        description: '是否使用高精度模式',
        default: false,
        note: '高精度模式耗电更多,响应更慢'
    },
    timeout: {
        description: '请求超时时间(毫秒)',
        default: Infinity,
        note: '超时后会触发错误回调'
    },
    maximumAge: {
        description: '缓存位置的最大时间(毫秒)',
        default: 0,
        note: '0表示不使用缓存,Infinity表示始终使用缓存'
    }
};

持续监听

watchPosition

const watchId = navigator.geolocation.watchPosition(
    (position) => {
        console.log('位置更新:', position.coords);
    },
    (error) => {
        console.error('监听位置失败:', error);
    },
    {
        enableHighAccuracy: true,
        maximumAge: 30000
    }
);

navigator.geolocation.clearWatch(watchId);

封装位置监听

class LocationTracker {
    constructor(options = {}) {
        this.options = {
            enableHighAccuracy: true,
            timeout: 10000,
            maximumAge: 0,
            ...options
        };
        this.watchId = null;
        this.listeners = [];
        this.lastPosition = null;
    }
    
    start() {
        if (this.watchId !== null) return;
        
        this.watchId = navigator.geolocation.watchPosition(
            (position) => {
                this.lastPosition = position;
                this.notifyListeners(position);
            },
            (error) => {
                this.notifyError(error);
            },
            this.options
        );
    }
    
    stop() {
        if (this.watchId !== null) {
            navigator.geolocation.clearWatch(this.watchId);
            this.watchId = null;
        }
    }
    
    onPosition(callback) {
        this.listeners.push({ type: 'position', callback });
        return () => this.off(callback);
    }
    
    onError(callback) {
        this.listeners.push({ type: 'error', callback });
        return () => this.off(callback);
    }
    
    off(callback) {
        this.listeners = this.listeners.filter(l => l.callback !== callback);
    }
    
    notifyListeners(position) {
        this.listeners
            .filter(l => l.type === 'position')
            .forEach(l => l.callback(position));
    }
    
    notifyError(error) {
        this.listeners
            .filter(l => l.type === 'error')
            .forEach(l => l.callback(error));
    }
    
    getLastPosition() {
        return this.lastPosition;
    }
}

const tracker = new LocationTracker();
tracker.onPosition((pos) => console.log('位置:', pos));
tracker.onError((err) => console.error('错误:', err));
tracker.start();

距离计算

Haversine公式

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

function toRad(deg) {
    return deg * (Math.PI / 180);
}

const distance = calculateDistance(39.9042, 116.4074, 31.2304, 121.4737);
console.log(`北京到上海的距离: ${distance.toFixed(2)} km`);

方位角计算

function calculateBearing(lat1, lon1, lat2, lon2) {
    const dLon = toRad(lon2 - lon1);
    const lat1Rad = toRad(lat1);
    const lat2Rad = toRad(lat2);
    
    const y = Math.sin(dLon) * Math.cos(lat2Rad);
    const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) -
              Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLon);
    
    let bearing = Math.atan2(y, x);
    bearing = toDeg(bearing);
    bearing = (bearing + 360) % 360;
    
    return bearing;
}

function toDeg(rad) {
    return rad * (180 / Math.PI);
}

地理围栏

class Geofence {
    constructor(center, radius, onEnter, onExit) {
        this.center = center;
        this.radius = radius;
        this.onEnter = onEnter;
        this.onExit = onExit;
        this.isInside = false;
    }
    
    checkPosition(position) {
        const distance = calculateDistance(
            position.coords.latitude,
            position.coords.longitude,
            this.center.lat,
            this.center.lng
        );
        
        const wasInside = this.isInside;
        this.isInside = distance <= this.radius;
        
        if (!wasInside && this.isInside) {
            this.onEnter?.(position, distance);
        } else if (wasInside && !this.isInside) {
            this.onExit?.(position, distance);
        }
        
        return this.isInside;
    }
}

class GeofenceManager {
    constructor() {
        this.geofences = new Map();
        this.tracker = new LocationTracker();
    }
    
    addGeofence(id, center, radius, callbacks) {
        const geofence = new Geofence(
            center,
            radius,
            callbacks.onEnter,
            callbacks.onExit
        );
        this.geofences.set(id, geofence);
    }
    
    removeGeofence(id) {
        this.geofences.delete(id);
    }
    
    start() {
        this.tracker.onPosition((position) => {
            this.geofences.forEach((geofence, id) => {
                geofence.checkPosition(position);
            });
        });
        this.tracker.start();
    }
    
    stop() {
        this.tracker.stop();
    }
}

const geofenceManager = new GeofenceManager();

geofenceManager.addGeofence('office', 
    { lat: 39.9042, lng: 116.4074 }, 
    0.1,
    {
        onEnter: () => console.log('进入办公室区域'),
        onExit: () => console.log('离开办公室区域')
    }
);

geofenceManager.start();

地图集成

function initMap(position) {
    const map = new google.maps.Map(document.getElementById('map'), {
        center: {
            lat: position.coords.latitude,
            lng: position.coords.longitude
        },
        zoom: 15
    });
    
    new google.maps.Marker({
        position: {
            lat: position.coords.latitude,
            lng: position.coords.longitude
        },
        map: map,
        title: '当前位置'
    });
}

async function showOnMap() {
    try {
        const position = await new Promise((resolve, reject) => {
            navigator.geolocation.getCurrentPosition(resolve, reject);
        });
        initMap(position);
    } catch (error) {
        console.error('无法获取位置:', error);
    }
}

东巴文小贴士

📍 位置隐私

  • 用户必须明确授权才能获取位置
  • 只在需要时请求位置权限
  • 提供不依赖位置的备选方案
  • 清晰说明位置数据的用途

性能建议

  • 使用maximumAge减少GPS请求
  • 不需要高精度时关闭enableHighAccuracy
  • 及时clearWatch停止监听
  • 考虑使用IP定位作为备选方案

下一步

下一章将探讨 [Drag and Drop](file:///e:/db-w.cn/md_data/javascript/79_Drag and Drop.md),学习如何在Web应用中实现拖放功能。