带有 GPS 记录功能的运动手表价格不菲,于是我自己写了一个简单的运动轨迹记录工具。核心代码如下,只有不到 200 行。
工具记录的数据可以使用 Isolated Storage Explorer Tool 导出,进行进一步的分析、处理和转换。
下载链接 http://pan.baidu.com/s/1sjO43s5
using System; using System.Collections.Generic; using System.ComponentModel; using System.Device.Location; using System.IO; using System.IO.IsolatedStorage; using System.Text; using System.Windows; using Microsoft.Phone.Controls; using Microsoft.Phone.Shell; namespace Tracker { public partial class MainPage : PhoneApplicationPage { bool tracking; string logName; GeoCoordinateWatcher watcher; List<GeoCoordinate> locations; List<long> timestamps; Object lockObject; public MainPage() { tracking = false; lockObject = new Object(); InitializeComponent(); PhoneApplicationService.Current.ApplicationIdleDetectionMode = IdleDetectionMode.Disabled; updateLogName(); status.Text = "GPS Tracker by coolypf"; watcher = new GeoCoordinateWatcher(GeoPositionAccuracy.High); watcher.MovementThreshold = 0.0; watcher.PositionChanged += onMove; } void onMove(object sender, GeoPositionChangedEventArgs<GeoCoordinate> e) { if (!tracking || watcher.Status != GeoPositionStatus.Ready) return; lock (lockObject) { locations.Add(e.Position.Location); timestamps.Add(e.Position.Timestamp.UtcTicks); } } void updateLogName() { IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication(); DateTime now = DateTime.Now; String prefix = now.Year + "-" + now.Month + "-" + now.Day + "-"; String filename; int idx = 1; while (true) { filename = prefix + idx + ".txt"; if (!isf.FileExists(filename)) break; idx++; } name.Text = filename; } void writeLog() { lock (lockObject) { if (locations.Count < 2) return; IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication(); StreamWriter w = new StreamWriter(isf.OpenFile(logName, FileMode.Create, FileAccess.Write)); for (int i = 0; i < locations.Count; ++i) { w.Write(timestamps[i]); w.Write(" "); GeoCoordinate g = locations[i]; w.Write(g.Latitude); w.Write(" "); w.Write(g.Longitude); w.Write(" "); w.Write(g.Altitude); w.WriteLine(); } w.Close(); } } void onControl(object sender, RoutedEventArgs e) { control.IsEnabled = false; if (tracking) { watcher.Stop(); writeLog(); status.Text = getStatus(); control.Content = "Start"; show.IsEnabled = false; updateLogName(); name.IsEnabled = true; tracking = false; } else { name.IsEnabled = false; logName = name.Text; locations = new List<GeoCoordinate>(); timestamps = new List<long>(); tracking = true; watcher.Start(); control.Content = "Stop"; show.IsEnabled = true; } control.IsEnabled = true; } String getStatus() { StringBuilder b = new StringBuilder(); lock (lockObject) { b.Append("Tracked positions: "); b.Append(locations.Count); b.AppendLine(); if (locations.Count >= 2) { double dist = 0.0, dist_r = 0.0, speed = 0.0; long ticks = timestamps[timestamps.Count - 1] - timestamps[0], ticks_r = 0; for (int i = 1; i < locations.Count; ++i) { double dd = locations[i - 1].GetDistanceTo(locations[i]); long dt = timestamps[i] - timestamps[i - 1]; if (dt > 0) speed = dd * 1e7 / dt; if (speed > 2.0) { dist_r += dd; ticks_r += dt; } dist += dd; } b.Append("Total distance: "); b.Append(dist * 1e-3); b.AppendLine(" km"); b.Append("Total time: "); b.Append(new TimeSpan(ticks).ToString()); b.AppendLine(); b.Append("Average speed: "); if (ticks > 0) speed = dist * 3.6e7 / ticks; else speed = 0.0; b.Append(speed); b.AppendLine(" km/h"); b.Append("Running speed: "); if (ticks_r > 0) speed = dist_r * 3.6e7 / ticks_r; else speed = 0.0; b.Append(speed); b.AppendLine(" km/h"); } } return b.ToString(); } void onShow(object sender, RoutedEventArgs e) { if (!tracking) return; status.Text = getStatus() + "GPS status: " + watcher.Status.ToString(); } protected override void OnBackKeyPress(CancelEventArgs e) { if (tracking) { MessageBoxResult result = MessageBox.Show("Tracking is in progress. If you quit, all recorded data will be lost.", "Quit Tracker?", MessageBoxButton.OKCancel); if (result != MessageBoxResult.OK) e.Cancel = true; } base.OnBackKeyPress(e); } } }
下面的程序可以处理轨迹数据,进行简单的分析,并将数据转换成 CSV 和 GPX 格式。前者可以用 Excel 打开,后者可以导入 Google Earth 中,并进一步转换成 KMZ 格式导入到 Google Maps 里面。
#include <stdio.h> #include <string.h> #include <math.h> #include <vector> using std::vector; static double distance(double lat1, double lon1, double lat2, double lon2); static void evil_transform(double wg_lat, double wg_lon, double &mg_lat, double &mg_lon); static void bd_encrypt(double gg_lat, double gg_lon, double &bd_lat, double &bd_lon); static void change_extension(char *name, const char *ext); static int date_time(long long ticks, int part); struct Record { long long tick; double lat, lon, alt, speed; }; struct Data { long long tick; double lat, lon, alt, speed; double time, dist, acc; }; const double running_threshold = 1.9; const double walking_threshold = 0.1; static inline int hour(long long ticks) { return (int)((ticks & 0x3fffffffffffffffll) / 0x861c46800ll % 24ll); } static inline int minute(long long ticks) { return (int)((ticks & 0x3fffffffffffffffll) / 0x23c34600ll % 60ll); } static inline int second(long long ticks) { return (int)((ticks & 0x3fffffffffffffffll) / 0x989680ll % 60ll); } static inline int millisecond(long long ticks) { return (int)((ticks & 0x3fffffffffffffffll) / 10000 % 1000); } int main(int argc, char **argv) { double sampling_threshold = 2.0; if (argc < 2) { fprintf(stderr, "Usage: analyzer tracking_log.txt [sampling_threshold]\nDefault sampling threshold is 2.0 seconds.\n"); return 1; } if (argc >= 3) { double arg = atof(argv[2]); if (arg > 0.0 && arg < 3600.0) sampling_threshold = arg; } if (strlen(argv[1]) > 4000) { fprintf(stderr, "File name too long!\n"); return 1; } // Read tracking log records static char buf[4096]; FILE *fp = fopen(argv[1], "r"); if (!fp) { fprintf(stderr, "Can not open %s\n", argv[1]); return 1; } vector<Record> vr; while (1) { if (!fgets(buf, 4096, fp)) break; int len = (int)strlen(buf); if (len < 10 || len > 4000) { fprintf(stderr, "Invalid line: %d\n", (int)vr.size() + 1); return 1; } Record rec; if (sscanf(buf, "%lld%lf%lf%lf", &rec.tick, &rec.lat, &rec.lon, &rec.alt) != 4) { fprintf(stderr, "Invalid line: %d\n", (int)vr.size() + 1); return 1; } vr.push_back(rec); } fclose(fp); // Filter invalid records if (vr.size() < 2) { fprintf(stderr, "No enough data to analyze!\n"); return 1; } vr[0].speed = 0.0; for (size_t i = 1; i < vr.size(); ++i) { double dd = distance(vr[i - 1].lat, vr[i - 1].lon, vr[i].lat, vr[i].lon); long long dt = vr[i].tick - vr[i - 1].tick; if (dt > 0) vr[i].speed = dd * 1e7 / dt; else vr[i].speed = vr[i - 1].speed; } vector<Record> v2; double speed_avg = 0.0, speed_devi = 0.0, speed_limit; for (size_t i = 0; i < vr.size(); ++i) speed_avg += vr[i].speed; speed_avg /= vr.size(); for (size_t i = 0; i < vr.size(); ++i) speed_devi += (vr[i].speed - speed_avg) * (vr[i].speed - speed_avg); speed_devi = sqrt(speed_devi / vr.size()); speed_limit = speed_avg + 3.0 * speed_devi; // fprintf(stderr, "speed avg, devi, limit: %.2lf, %.2lf, %.2lf\n", speed_avg, speed_devi, speed_limit); for (size_t i = 0; i < vr.size(); ++i) if (vr[i].speed < speed_limit) v2.push_back(vr[i]); vr.swap(v2); v2.clear(); // Resample data if (vr.size() < 2) { fprintf(stderr, "No enough data to analyze!\n"); return 1; } vector<Data> vd; { Data dat0; dat0.tick = vr[0].tick; dat0.lat = vr[0].lat; dat0.lon = vr[0].lon; dat0.alt = vr[0].alt; dat0.speed = 0.0; dat0.time = 0.0; dat0.dist = 0.0; dat0.acc = 0.0; vd.push_back(dat0); } long long begin = vr[0].tick; double prev_time = 0.0, dist = 0.0; for (size_t i = 1; i < vr.size(); ++i) { double time = (vr[i].tick - begin) * 1e-7; if (i + 1 < vr.size() && time - prev_time < sampling_threshold) continue; long long dt = vr[i].tick - vd.back().tick; if (dt <= 0) continue; Data dat; double dd = distance(vd.back().lat, vd.back().lon, vr[i].lat, vr[i].lon); dist += dd; dat.tick = vr[i].tick; dat.lat = vr[i].lat; dat.lon = vr[i].lon; dat.alt = vr[i].alt; dat.speed = dd * 1e7 / dt; dat.time = time; dat.dist = dist; vd.push_back(dat); prev_time = time; } vr.clear(); // Analyze data if (vd.size() < 2) { fprintf(stderr, "No enough data to analyze!\n"); return 1; } for (size_t i = 1; i + 1 < vd.size(); ++i) { vd[i].acc = (vd[i + 1].speed - vd[i].speed) * 2e7 / (vd[i + 1].tick - vd[i - 1].tick); vd[i].speed = (vd[i].speed * (vd[i + 1].tick - vd[i].tick) + vd[i + 1].speed * (vd[i].tick - vd[i - 1].tick)) / (vd[i + 1].tick - vd[i - 1].tick); } vd.back().speed = 0.0; vd.back().acc = 0.0; double dist_r = 0.0, mx_speed = 0.0; long long time_r = 0, time_w = 0; for (size_t i = 1; i < vd.size(); ++i) { double dd = vd[i].dist - vd[i - 1].dist; long long dt = vd[i].tick - vd[i - 1].tick; if (vd[i].speed > running_threshold) { dist_r += dd; time_r += dt; if (vd[i].speed > mx_speed) mx_speed = vd[i].speed; } else if (vd[i].speed > walking_threshold) time_w += dt; } double dist_a = vd.back().dist, dist_w = dist_a - dist_r; long long time_a = vd.back().tick - vd.front().tick, time_s = time_a - time_r - time_w; printf("%30s: %.3lf km\n", "Total distance", dist_a * 1e-3); printf("%30s: %.3lf km\n", "Running distance", dist_r * 1e-3); printf("%30s: %.3lf km\n", "Walking distance", dist_w * 1e-3); printf("%30s: %d h %d m %d.%03d s\n", "Total time", hour(time_a), minute(time_a), second(time_a), millisecond(time_a)); printf("%30s: %d h %d m %d.%03d s\n", "Running time", hour(time_r), minute(time_r), second(time_r), millisecond(time_r)); printf("%30s: %d h %d m %d.%03d s\n", "Walking time", hour(time_w), minute(time_w), second(time_w), millisecond(time_w)); printf("%30s: %d h %d m %d.%03d s\n", "Stopped time", hour(time_s), minute(time_s), second(time_s), millisecond(time_s)); printf("%30s: %.2lf m/s , %.2lf km/h\n", "Max speed", mx_speed, mx_speed * 3.6); printf("%30s: %.2lf m/s , %.2lf km/h\n", "Average speed", dist_a * 1e7 / time_a, dist_a * 3.6e7 / time_a); printf("%30s: %.2lf m/s , %.2lf km/h\n", "Average running speed", dist_r * 1e7 / time_r, dist_r * 3.6e7 / time_r); printf("%30s: %.2lf m/s , %.2lf km/h\n", "Average walking speed", dist_w * 1e7 / time_w, dist_w * 3.6e7 / time_w); // Export data to CSV strcpy(buf, argv[1]); change_extension(buf, ".csv"); fp = fopen(buf, "w"); if (!fp) { fprintf(stderr, "Can not write to %s\n", buf); return 1; } fprintf(fp, "Time(s),Distance(km),Altitude(m),Speed(m/s),Speed(km/h),Acceleration(m/s^2)\n"); for (size_t i = 0; i < vd.size(); ++i) fprintf(fp, "%.2lf,%.3lf,%.1lf,%.2lf,%.2lf,%.2lf\n", vd[i].time, vd[i].dist * 1e-3, vd[i].alt, vd[i].speed, vd[i].speed * 3.6, vd[i].acc); fclose(fp); // Export data to GPX const char gpx_head[] = "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\" ?>\n\n" "<gpx xmlns=\"http://www.topografix.com/GPX/1/1\" version=\"1.1\" creator=\"Analyzer for WP7Tracker\">\n" "<metadata><name>%s</name><link href=\"http://blog.csdn.net/coolypf/\"><text>coolypf</text></link></metadata>\n" "<trk><name>%s</name><trkseg>\n"; const char gpx_tail[] = "</trkseg></trk></gpx>\n"; strcpy(buf, argv[1]); change_extension(buf, ".wgs-84.gpx"); fp = fopen(buf, "w"); if (!fp) { fprintf(stderr, "Can not write to %s\n", buf); return 1; } fprintf(fp, gpx_head, buf, buf); for (size_t i = 0; i < vd.size(); ++i) { fprintf(fp, "<trkpt lat=\"%.6lf\" lon=\"%.6lf\"><ele>%.1lf</ele>", vd[i].lat, vd[i].lon, vd[i].alt); long long tick = vd[i].tick; fprintf(fp, "<time>%04d-%02d-%02dT%02d:%02d:%02dZ</time></trkpt>\n", date_time(tick, 0), date_time(tick, 2), date_time(tick, 3), hour(tick), minute(tick), second(tick)); } fprintf(fp, gpx_tail); fclose(fp); strcpy(buf, argv[1]); change_extension(buf, ".gcj-02.gpx"); fp = fopen(buf, "w"); if (!fp) { fprintf(stderr, "Can not write to %s\n", buf); return 1; } fprintf(fp, gpx_head, buf, buf); for (size_t i = 0; i < vd.size(); ++i) { double lat, lon; evil_transform(vd[i].lat, vd[i].lon, lat, lon); vd[i].lat = lat; vd[i].lon = lon; fprintf(fp, "<trkpt lat=\"%.6lf\" lon=\"%.6lf\"><ele>%.1lf</ele>", vd[i].lat, vd[i].lon, vd[i].alt); long long tick = vd[i].tick; fprintf(fp, "<time>%04d-%02d-%02dT%02d:%02d:%02dZ</time></trkpt>\n", date_time(tick, 0), date_time(tick, 2), date_time(tick, 3), hour(tick), minute(tick), second(tick)); } fprintf(fp, gpx_tail); fclose(fp); strcpy(buf, argv[1]); change_extension(buf, ".bd-09.gpx"); fp = fopen(buf, "w"); if (!fp) { fprintf(stderr, "Can not write to %s\n", buf); return 1; } fprintf(fp, gpx_head, buf, buf); for (size_t i = 0; i < vd.size(); ++i) { double lat, lon; bd_encrypt(vd[i].lat, vd[i].lon, lat, lon); vd[i].lat = lat; vd[i].lon = lon; fprintf(fp, "<trkpt lat=\"%.6lf\" lon=\"%.6lf\"><ele>%.1lf</ele>", vd[i].lat, vd[i].lon, vd[i].alt); long long tick = vd[i].tick; fprintf(fp, "<time>%04d-%02d-%02dT%02d:%02d:%02dZ</time></trkpt>\n", date_time(tick, 0), date_time(tick, 2), date_time(tick, 3), hour(tick), minute(tick), second(tick)); } fprintf(fp, gpx_tail); fclose(fp); return 0; } const double pi = 3.14159265358979324; const double f = pi / 180.0; const double x_pi = f * 3000.0; const double a = 6378245.0; const double ee = 0.00669342162296594323; const double r = 6376500.0; double distance(double lat1, double lon1, double lat2, double lon2) { double rad1 = lat1 * f, rad2 = lat2 * f; double dlat = rad2 - rad1, dlon = (lon2 - lon1) * f; double x = pow(sin(dlat / 2.0), 2.0) + (cos(rad1) * cos(rad2)) * pow(sin(dlon / 2.0), 2.0); return r * 2.0 * atan2(sqrt(x), sqrt(1.0 - x)); } static bool out_of_mars(double lat, double lon) { if (lon < 72.004 || lon > 137.8347) return true; if (lat < 0.8293 || lat > 55.8271) return true; return false; } static double transform_lat(double x, double y) { double ret = -100.0 + 2.0 * x + 3.0 * y + 0.2 * y * y + 0.1 * x * y + 0.2 * sqrt(fabs(x)); ret += (20.0 * sin(6.0 * x * pi) + 20.0 * sin(2.0 * x * pi)) * 2.0 / 3.0; ret += (20.0 * sin(y * pi) + 40.0 * sin(y / 3.0 * pi)) * 2.0 / 3.0; ret += (160.0 * sin(y / 12.0 * pi) + 320 * sin(y * pi / 30.0)) * 2.0 / 3.0; return ret; } static double transform_lon(double x, double y) { double ret = 300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(fabs(x)); ret += (20.0 * sin(6.0 * x * pi) + 20.0 * sin(2.0 * x * pi)) * 2.0 / 3.0; ret += (20.0 * sin(x * pi) + 40.0 * sin(x / 3.0 * pi)) * 2.0 / 3.0; ret += (150.0 * sin(x / 12.0 * pi) + 300.0 * sin(x / 30.0 * pi)) * 2.0 / 3.0; return ret; } void evil_transform(double wg_lat, double wg_lon, double &mg_lat, double &mg_lon) { if (out_of_mars(wg_lat, wg_lon)) { mg_lat = wg_lat; mg_lon = wg_lon; return; } double d_lat = transform_lat(wg_lon - 105.0, wg_lat - 35.0); double d_lon = transform_lon(wg_lon - 105.0, wg_lat - 35.0); double rad_lat = wg_lat / 180.0 * pi; double magic = sin(rad_lat); magic = 1 - ee * magic * magic; double sqrt_magic = sqrt(magic); d_lat = (d_lat * 180.0) / ((a * (1 - ee)) / (magic * sqrt_magic) * pi); d_lon = (d_lon * 180.0) / (a / sqrt_magic * cos(rad_lat) * pi); mg_lat = wg_lat + d_lat; mg_lon = wg_lon + d_lon; } void bd_encrypt(double gg_lat, double gg_lon, double &bd_lat, double &bd_lon) { double x = gg_lon, y = gg_lat; double z = sqrt(x * x + y * y) + 0.00002 * sin(y * x_pi); double theta = atan2(y, x) + 0.000003 * cos(x * x_pi); bd_lon = z * cos(theta) + 0.0065; bd_lat = z * sin(theta) + 0.006; } void change_extension(char *name, const char *ext) { int i = (int)strlen(name); while (1) { if (i <= 0 || name[i] == '/' || name[i] =='\\') { i = (int)strlen(name); break; } if (name[i] == '.') break; --i; } strcpy(name + i, ext); } int date_time(long long ticks, int part) { const int days_to_month_365[] = { 0, 0x1f, 0x3b, 90, 120, 0x97, 0xb5, 0xd4, 0xf3, 0x111, 0x130, 0x14e, 0x16d }; const int days_to_month_366[] = { 0, 0x1f, 60, 0x5b, 0x79, 0x98, 0xb6, 0xd5, 0xf4, 0x112, 0x131, 0x14f, 0x16e }; ticks &= 0x3fffffffffffffffll; int num2 = (int)(ticks / 0xc92a69c000ll); int num3 = num2 / 0x23ab1; num2 -= num3 * 0x23ab1; int num4 = num2 / 0x8eac; if (num4 == 4) num4 = 3; num2 -= num4 * 0x8eac; int num5 = num2 / 0x5b5; num2 -= num5 * 0x5b5; int num6 = num2 / 0x16d; if (num6 == 4) num6 = 3; if (part == 0) return num3 * 400 + num4 * 100 + num5 * 4 + num6 + 1; num2 -= num6 * 0x16d; if (part == 1) return num2 + 1; const int *num_array = (num6 == 3 && (num5 != 0x18 || num4 == 3)) ? days_to_month_366 : days_to_month_365; int index = num2 >> 6; while (num2 >= num_array[index]) index++; if (part == 2) return index; return num2 - num_array[index - 1] + 1; }