You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter旅行追踪距离计算严重偏差问题排查与方案求助

位置追踪距离计算异常问题排查

问题描述

我正在实现一项位置追踪功能:

  • 用户行进时每10秒捕获一次实时位置,经纬度存储到本地数据库
  • 每10分钟调用API将数据同步到服务器
  • 每10秒计算上一次捕获位置与最新位置的距离,累加得到总距离

测试时出现严重偏差:

  • 实际行进约35公里,计算总距离约400公里
  • 再次测试实际行进约15公里,计算总距离约150公里

求指出代码问题,或提供可靠的路径追踪与总距离计算替代方案。

现有代码

定时器实现

Timer.periodic(const Duration(seconds: 10), (timer) async {
  await CommonMethods.getCurrentLocationDateTime();
});

位置捕获逻辑

static Future getCurrentLocationDateTime() async {
  _result = await Geolocator.getCurrentPosition(
    desiredAccuracy: LocationAccuracy.bestForNavigation,
  );
  if (_updatedLatitude == null && _updatedLongitude == null) {
    _previousLatitude = _result!.latitude;
    _previousLongitude = _result!.longitude;
  } else {
    _previousLatitude = _updatedLatitude;
    _previousLongitude = _updatedLongitude;
  }
  _updatedLatitude = _result!.latitude;
  _updatedLongitude = _result!.longitude;

  // Save data in a local variable
  Map<String, String> _currentLatLong = {
    "lat": _result!.latitude.toStringAsFixed(8),
    "long": _result!.longitude.toStringAsFixed(8),
  };
  CommonEntities.travelHistoryLatLng.add(_currentLatLong);

  // Update previous and current location names
  await placemarkFromCoordinates(_updatedLatitude!, _updatedLongitude!)
      .then((List<Placemark> placemarks) {
    _previousLocationName = (_currentLocationName == "")
        ? placemarks[0].name!
        : _currentLocationName;
    _currentLocationName = placemarks[0].subLocality!;
  }).catchError((e) {
    debugPrint(e.toString());
  });

  // Check if device is not moved for more than 2 hours
  // if not moved then stop the timer
  if (CommonEntities.travelHistoryLatLng.toSet().length == 1) {
    CommonEntities.idealStateTravelTimeSpent += 10;
    if (CommonEntities.idealStateTravelTimeSpent >= 7200) {
      CommonEntities.idealStateTravelTimeSpent = 0;
      // BackgroundLocation.stopLocationService();
      if (CommonEntities.travelTimer != null) {
        CommonEntities.travelTimer!.cancel();
      }
      CommonEntities.isTravelTimerActive = false;
      CommonEntities.workEndTime =
          CommonEntities.travelTrackingDetails["current_date_time"];
    }
  } else {
    CommonEntities.idealStateTravelTimeSpent = 0;
  }

  // Get Current date time of captured lat-longs
  _currentDateTime = DateTime.now();
  _currentDateTimeString = _currentDateTime.year.toString() +
      "-" +
      _currentDateTime.month.toString() +
      "-" +
      _currentDateTime.day.toString() +
      " " +
      _currentDateTime.hour.toString() +
      ":" +
      _currentDateTime.minute.toString();

  if (CommonEntities.workStartTime == "00:00") {
    CommonEntities.workStartTime = _currentDateTime.hour.toString() +
        ":" +
        _currentDateTime.minute.toString();
    // LocalSharedPreferences().setWorkStartTime();
  }

  // Update the total distance covered in 10 min
  updateDistanceTraveled();
  print("Total Distance Covered: " + CommonEntities.travelTrackingDetails["total_distance_covered"]);
  // Call API to save travel data in every 10 minutes.
  if (CommonEntities.totalTravelsecondsCount == 600) {
    // Calculate the time before 10 min and for time interval in post params
    String _currentTime = _currentDateTime.hour.toString() +
        ':' +
        _currentDateTime.minute.toString();
    DateTime _dateTimeBeforeTenMin =
        _currentDateTime.subtract(const Duration(minutes: 10));
    String _timeBeforeTenMin = _dateTimeBeforeTenMin.hour.toString() +
        ':' +
        _dateTimeBeforeTenMin.minute.toString();

    String _tempDistanceTravelled =
        CommonEntities.travelTrackingDetails["total_distance_covered"];
    CommonEntities.travelTrackingDetails["total_distance_covered"] = "";
    _distanceTravelled = 0.0;
    // Create post params value
    Map<String, dynamic> params = {
      'lat_long': jsonEncode(CommonEntities.travelHistoryLatLng),
      'date': _currentDateTime.day.toString() +
          '-' +
          _currentDateTime.month.toString() +
          '-' +
          _currentDateTime.year.toString() +
          ' ' +
          _currentDateTime.hour.toString() +
          ':' +
          _currentDateTime.minute.toString() +
          ':' +
          _currentDateTime.second.toString(),
      'distance_covered': _tempDistanceTravelled,
      'userID': await LocalSharedPreferences().getUserId(),
      'time_interval': _timeBeforeTenMin + '-' + _currentTime,
      'location_name': _currentLocationName,
      'status': 'progress',
    };

    // Call the API to save travel history of 10 min of server
    HomeProvider().saveTravelDetails(params);
    // await LocalSharedPreferences().saveTravelHistoryLatLng();

    // Reset the timer of 10 min, clear lat-long history of 10 min
    CommonEntities.totalTravelsecondsCount = 0;
    CommonEntities.travelHistoryLatLng = [];
  }
  CommonEntities.totalTravelsecondsCount += 10;

  // Save latlong and date time
  // BOC: commented logging of lat-long as the same is not required in final app
  // by shubham.bansal2
  AppLogs().logToFile(
      _result!.latitude.toString() + "-" + _result!.longitude.toString(),
      _currentDateTimeString);

  CommonEntities.travelTrackingDetails["updated_latitude"] =
      _updatedLatitude!.toStringAsFixed(8);
  CommonEntities.travelTrackingDetails["updated_longitude"] =
      _updatedLongitude!.toStringAsFixed(8);
  CommonEntities.travelTrackingDetails["updated_location_name"] =
      _currentLocationName;
  CommonEntities.travelTrackingDetails["previous_latitude"] =
      _previousLatitude!.toStringAsFixed(8);
  CommonEntities.travelTrackingDetails["previous_longitude"] =
      _previousLongitude!.toStringAsFixed(8);
  CommonEntities.travelTrackingDetails["previous_location_name"] =
      _previousLocationName;
  CommonEntities.travelTrackingDetails["total_distance_covered"] =
      _distanceTravelled.toStringAsFixed(8);
  CommonEntities.travelTrackingDetails["current_date_time"] =
      _currentDateTimeString;
}

距离计算逻辑

static updateDistanceTraveled() {
  var p = 0.017453292519943295;
  var c = cos;
  var a = 0.5 -
      c((_updatedLatitude! - _previousLatitude!) * p) / 2 +
      c(_previousLatitude! * p) *
          c(_updatedLatitude! * p) *
          (1 - c((_updatedLongitude! - _previousLongitude!) * p)) /
          2;
  _distanceTravelled = double.parse(CommonEntities.travelTrackingDetails["total_distance_covered"]);
  _distanceTravelled += (12742 * asin(sqrt(a))).toDouble();
  CommonEntities.travelTrackingDetails["total_distance_covered"] =
      _distanceTravelled.toStringAsFixed(5);
}

核心问题分析

  1. 定时器重复创建
    这是导致距离偏差10倍左右的最可能原因:如果启动定时器的代码被多次执行,会创建多个Timer.periodic实例,每10秒重复执行多次位置捕获与距离计算,总距离会被累加N次(N为定时器数量),与测试结果完全匹配。

  2. 静止状态判断逻辑错误
    使用CommonEntities.travelHistoryLatLng.toSet().length == 1判断静止是无效的:Dart中Map是引用类型,即使两个Map内容完全一致,也会被视为不同元素,toSet().length永远不会等于1(除非仅添加了一个元素),导致静止判断失效。

  3. 距离计算初始化异常
    10分钟同步后,CommonEntities.travelTrackingDetails["total_distance_covered"]被设置为空字符串,下一次调用updateDistanceTraveled时,double.parse("")会抛出FormatException,破坏正常计算流程。

  4. 自定义距离计算的潜在风险
    手动实现的大圆距离公式虽然逻辑正确,但存在单位混淆、精度丢失的风险,不如官方库方法可靠。

修复方案

1. 确保定时器唯一

启动前检查是否已有活跃定时器,避免重复创建:

if (CommonEntities.isTravelTimerActive) return;
CommonEntities.travelTimer = Timer.periodic(const Duration(seconds: 10), (timer) async {
  await CommonMethods.getCurrentLocationDateTime();
});
CommonEntities.isTravelTimerActive = true;

2. 修复静止状态判断

替换为基于实际距离的判断(使用Geolocator官方方法计算两点距离,单位为米):

// 计算当前与上一次位置的距离(单位:米)
double currentDistance = Geolocator.distanceBetween(
  _previousLatitude!,
  _previousLongitude!,
  _updatedLatitude!,
  _updatedLongitude!,
);

if (currentDistance < 5) { // 小于5米视为静止
  CommonEntities.idealStateTravelTimeSpent += 10;
  if (CommonEntities.idealStateTravelTimeSpent >= 7200) {
    CommonEntities.idealStateTravelTimeSpent = 0;
    if (CommonEntities.travelTimer != null) {
      CommonEntities.travelTimer!.cancel();
    }
    CommonEntities.isTravelTimerActive = false;
    CommonEntities.workEndTime = CommonEntities.travelTrackingDetails["current_date_time"];
  }
} else {
  CommonEntities.idealStateTravelTimeSpent = 0;
}

3. 修复距离计算初始化问题

同步后将总距离重置为"0.0"而非空字符串:

// 替换原有的空字符串设置
CommonEntities.travelTrackingDetails["total_distance_covered"] = "0.0";

4. 替换自定义距离计算为官方方法

使用Geolocator的distanceBetween方法,避免手动实现的潜在问题:

static updateDistanceTraveled() {
  // 计算两点距离(单位:米)
  double distanceMeters = Geolocator.distanceBetween(
    _previousLatitude!,
    _previousLongitude!,
    _updatedLatitude!,
    _updatedLongitude!,
  );
  // 转换为公里(根据需求调整单位)
  double distanceKm = distanceMeters / 1000;

  _distanceTravelled = double.parse(CommonEntities.travelTrackingDetails["total_distance_covered"]);
  _distanceTravelled += distanceKm;
  CommonEntities.travelTrackingDetails["total_distance_covered"] = _distanceTravelled.toStringAsFixed(5);
}

5. 简化日期时间处理

使用intl包的DateFormat类,避免手动拼接的格式错误:

import 'package:intl/intl.dart';

// 格式化日期时间
_currentDateTime = DateTime.now();
_currentDateTimeString = DateFormat('yyyy-MM-dd HH:mm').format(_currentDateTime);
// 同步API用的完整日期时间
String fullDateTime = DateFormat('dd-MM-yyyy HH:mm:ss').format(_currentDateTime);

替代方案

如果需要更可靠的路径追踪,可考虑以下优化:

  • 使用位置流监听:替换定时调用getCurrentPosition为Geolocator.getPositionStream,精准捕获位置变化,避免定时器时间误差。
  • 过滤低精度位置:通过Position.accuracy字段过滤精度差的位置数据,避免异常点导致距离偏差。
  • 本地持久化存储:使用Hive、SQFlite等本地数据库存储位置数据,避免App后台被杀死后数据丢失。

内容的提问来源于stack exchange,提问作者Shubham Bansal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.09 04:35:20