Bloom_Ware / static /frontend /js /location.js
LawrenceBai's picture
feat: overhaul MCP architecture with structured tool schemas, comprehensive care-mode skill definitions, and enhanced test coverage for pipelines and service integration.
79df050
Raw
History Blame Contribute Delete
6.29 kB
let watchId = null;
let lastPosition = null;
let lastSentPosition = null; // 上次發送的位置
let lastSendTime = 0; // 上次發送時間
let isTracking = false;
let isIPFallbackTriggered = false; // 避免 IP 定位服務重複觸發
const MIN_SEND_INTERVAL = 60000; // 最小發送間隔:60 秒
const MIN_DISTANCE_CHANGE = 100; // 最小距離變化:100 米
async function startLocationTracking() {
if (isTracking) {
return;
}
if (!navigator.geolocation) {
console.warn('⚠️ 此瀏覽器不支援定位功能');
return;
}
try {
const position = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: false, // 不需要高精度(省電)
timeout: 10000,
maximumAge: 60000 // 接受 1 分鐘內的快取位置
});
});
handlePositionUpdate(position);
watchId = navigator.geolocation.watchPosition(
handlePositionUpdate,
handlePositionError,
{
enableHighAccuracy: false,
timeout: 10000,
maximumAge: 60000
}
);
isTracking = true;
} catch (error) {
handlePositionError(error);
}
}
function stopLocationTracking() {
if (watchId !== null) {
navigator.geolocation.clearWatch(watchId);
watchId = null;
isTracking = false;
}
}
function calculateDistance(lat1, lon1, lat2, lon2) {
const R = 6371000; // 地球半徑(米)
const dLat = (lat2 - lat1) * Math.PI / 180;
const dLon = (lon2 - lon1) * Math.PI / 180;
const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
Math.sin(dLon/2) * Math.sin(dLon/2);
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
return R * c;
}
function shouldSendUpdate(newPosition) {
const now = Date.now();
if (!lastSentPosition) return true;
const timeSinceLast = now - lastSendTime;
if (timeSinceLast < MIN_SEND_INTERVAL) return false;
const distance = calculateDistance(
lastSentPosition.lat, lastSentPosition.lon,
newPosition.lat, newPosition.lon
);
return distance >= MIN_DISTANCE_CHANGE;
}
function handlePositionUpdate(position) {
const { latitude, longitude, accuracy, heading, speed } = position.coords;
const timestamp = position.timestamp;
lastPosition = {
lat: latitude,
lon: longitude,
accuracy: accuracy,
heading: heading || 0,
speed: speed || 0,
timestamp: timestamp
};
if (shouldSendUpdate(lastPosition)) {
sendEnvironmentSnapshot(lastPosition);
lastSentPosition = { ...lastPosition };
lastSendTime = Date.now();
}
}
function handlePositionError(error) {
let errorMessage = '';
switch (error.code) {
case error.PERMISSION_DENIED:
errorMessage = '用戶拒絕定位權限';
console.warn('⚠️ 位置權限被拒絕,部分功能(如查詢附近公車)將無法使用');
break;
case error.POSITION_UNAVAILABLE:
errorMessage = '無法取得位置資訊';
console.warn('⚠️ 位置資訊暫時無法取得');
triggerIPFallback();
break;
case error.TIMEOUT:
errorMessage = '定位請求逾時';
console.warn('⚠️ 定位請求逾時');
triggerIPFallback();
break;
default:
errorMessage = '未知錯誤';
console.warn('⚠️ 定位發生未知錯誤:', error);
}
// 避免在嘗試 IP fallabck 成功前就送出 null
if (!isIPFallbackTriggered || error.code === error.PERMISSION_DENIED) {
sendEnvironmentSnapshot({
lat: null,
lon: null,
error: errorMessage,
timestamp: Date.now()
});
}
}
async function triggerIPFallback() {
if (isIPFallbackTriggered) return;
isIPFallbackTriggered = true;
try {
const res = await fetch('https://ipapi.co/json/');
if (!res.ok) throw new Error(`IP Geo API HTTP ${res.status}`);
const data = await res.json();
if (data.latitude && data.longitude) {
console.log(`📍 [GeoIP] IP 定位備援成功 (${data.city}, ${data.country_name})`);
const ipPosition = {
coords: {
latitude: data.latitude,
longitude: data.longitude,
accuracy: 5000,
heading: null,
speed: null
},
timestamp: Date.now()
};
// 利用模擬的 Position 物件更新系統
handlePositionUpdate(ipPosition);
return;
}
} catch (err) {
console.warn('⚠️ [GeoIP] IP 定位備援同樣失敗:', err);
}
// Fallback 失敗時依然丟回最後的 snapshot(帶著錯誤狀態)
sendEnvironmentSnapshot({
lat: null,
lon: null,
error: '精準定位與 IP 定位均失敗',
timestamp: Date.now()
});
}
function sendEnvironmentSnapshot(positionData) {
if (!wsManager || !wsManager.isConnected()) {
return; // 靜默跳過
}
const snapshot = {
lat: positionData.lat,
lon: positionData.lon,
accuracy_m: positionData.accuracy,
heading_deg: positionData.heading,
speed: positionData.speed,
timestamp: positionData.timestamp || Date.now(),
tz: Intl.DateTimeFormat().resolvedOptions().timeZone,
locale: navigator.language,
device: {
user_agent: navigator.userAgent,
platform: navigator.platform,
screen_width: window.screen.width,
screen_height: window.screen.height,
viewport_width: window.innerWidth,
viewport_height: window.innerHeight
},
error: positionData.error || null
};
wsManager.send({ type: 'env_snapshot', ...snapshot });
if (window.DEBUG_MODE) {
}
}
async function requestLocationUpdate() {
if (!navigator.geolocation) {
console.warn('⚠️ 此瀏覽器不支援定位功能');
return null;
}
try {
const position = await new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true, // 手動請求時使用高精度
timeout: 10000,
maximumAge: 0 // 不使用快取
});
});
handlePositionUpdate(position);
return lastPosition;
} catch (error) {
handlePositionError(error);
return null;
}
}
function getLastKnownPosition() {
return lastPosition;
}