Coding4Fun–GPS心跳表記錄檔轉TCX

RunKeeper是個有趣的健身社群網站,在WP7、iPhone、Android等手機平台都有App,利用手機GPS定位邊慢跑邊記錄,能算出時速、里程、消耗卡路里等,之後還可上傳到網站,產生精美報告,結合Google Map呈現跑步路線,提供坡度爬升、平均速度的曲線圖,最後還可將結果分享到Facebook、Twitter等社群平台,平添運動健身的樂趣! 除了透過App記錄,RunKeeper也支援GPS記錄檔上傳,換句話說,GPS手錶或其他導航裝置也可當作RunKeeper的記錄工具,甚至比App再多出心跳記錄功能。

之前試過抓著HD7執行RunKeeper App跑完政大環山道,邊跑邊看速度讓慢跑更有趣,只是抓著有點重量的手機跑步,總要擔心一個手滑摔機,會淚灑跑道,難免礙手礙腳,無法盡情奔跑。之前看過有人用手機臂套把iPhone綁在手臂上,不過手臂多掛162公克的重物,多少會影響平衡,而臂套必須束緊防滑,會造成一些不適感,也不算很完美的方案。現在有了GPS手錶,就方便多了。

話說新入手的心跳錶雖然便宜大碗,但畢竟難比國際大廠,整合應用上不如Garmin產品吃香。以RunKeeper的GPS記錄檔匯入功能為例,它支援GPXTCX兩種格式(TCX為Garmin發展的規格,除地理資訊外,還可包含心跳率、自行車踏頻等資料),Gramin裝置可直接匯出整合。而GH-625M搭配的是自家的GS-Sport Traning Gym軟體,可由手錶載入記錄、繪製Google Map路線圖及速度、高度、心跳曲線圖,並能依日期歸檔,功能尚可,但不如RunKeeper報表來得精美,且少了社群分享功能也少了幾分趣味。

GS-Sport Training Gym可將跑步歷程匯出成GPX格式,但心跳率部分使用自創的XML Schema儲存,RunKeeper無法識別。而在Garmin的規格中,TCX才支援心跳,於是我想到若能將其轉為TCX格式,就可實現連同心跳資料一起匯入RunKeeper的夢想。

做了簡單的研究:

  1. 關於TCX,Garmin提供了很詳細的規格,還有Sample(TCX Fitness Courses Detail)可參考,要做出符合規格的XML不難。
  2. GS-Sport Training Gym除了可將運動路線匯出成GPX外,還有一個更有趣的匯出格式ACT,用Notepad++打開ACT檔一看:
     
    有沒有"他鄉遇故知"的激動感呀? 這不就是.NET DataSet的XML儲存格式嗎? 小試之後,確認用DataSet.LoadXml()就能將它還原回內含三個DataTable的.NET DataSet物件,處理起來比GPX還方便。(猜想GS-Sport Training Gym應該是用.NET開發的)

此等天時地利人和,還不寫個轉檔程式自用,豈不人神共憤,天地難容,沒資格被稱作資深.NET開發人員?

所以,不到150行,TCX轉檔程式就完成囉! 編譯成WinForm型式,就仿照【潛盾機】避免Excel開啟CSV時截掉左補零的小工具原理,執行後選取ACT檔案,轉成TCX後存於同一目錄下。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data;
using System.IO;
using System.Windows.Forms;
 
namespace Act2Tcx
{
    class Program
    {
        [STAThread]
        static void Main(string[] args)
        {
            OpenFileDialog openFile = new OpenFileDialog();
            openFile.Filter = "GS-Sport ACT File|*.act";
            if (openFile.ShowDialog() == DialogResult.OK)
            {
                Course c = Load(openFile.FileName);
                string tcxFile = Path.Combine(
                    Path.GetDirectoryName(openFile.FileName), 
                    c.Name.Replace(":", "") + ".tcx");
                File.WriteAllText(tcxFile, c.ToXml());
                MessageBox.Show("Done! - " + tcxFile);
            }
        }
 
        public static Course Load(string file)
        {
            DataSet ds = new DataSet();
            ds.ReadXml(file);
            DataTable trackmaster = ds.Tables["trackmaster"];
            DataTable trackPoints = ds.Tables["TrackPoints"];
            DataRow row = trackmaster.Rows[0];
            DateTime startTime = DateTime.ParseExact(
            row["TrackName"].ToString() + " " + row["StartTime"].ToString(),
                "yyyy-M-dd HH:mm:ss", null);
            Course c = new Course()
            {
                Name = string.Format("{0:yyyy-MM-dd HH:mm:ss} {1:N0}m", startTime,
                       decimal.Parse(row["Distence"].ToString()))
            };
            DateTime currTime = startTime;
            foreach (DataRow tp in trackPoints.Rows)
            {
                c.TrackPoints.Add(new TrackPoint()
                {
                    Time = currTime,
                    AltitudeMeters = decimal.Parse(tp["Altitude"].ToString()),
                    HeartRateBpm = decimal.Parse(tp["HeartRate"].ToString()),
                    LatitudeDegrees = decimal.Parse(tp["Latitude"].ToString()),
                    LongitudeDegrees = decimal.Parse(tp["Longitude"].ToString())
                });
                currTime = currTime.AddSeconds(double.Parse(tp["IntervalTime"].ToString()));
            }
            return c;
        }
    }
 
    public class Course
    {
        public string Name;
        public List<TrackPoint> TrackPoints = new List<TrackPoint>();
        public string ToXml()
        {
            StringBuilder sb = new StringBuilder();
            TrackPoint st = TrackPoints.First(), ed = TrackPoints.Last();
            sb.AppendFormat(@"<?xml version=""1.0"" encoding=""UTF-8"" standalone=""no"" ?>
<TrainingCenterDatabase xmlns=""http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2"" xmlns:xsi=""http://www.w3.org/2001/XMLSchema-instance"" xsi:schemaLocation=""http://www.garmin.com/xmlschemas/TrainingCenterDatabase/v2 http://www.garmin.com/xmlschemas/TrainingCenterDatabasev2.xsd"">
  <Folders/>
  <Courses>
    <Course>
      <Name>{0}</Name>
      <Lap>
        <TotalTimeSeconds>{1}</TotalTimeSeconds>
        <DistanceMeters>{2}</DistanceMeters>
        <BeginPosition>
          <LatitudeDegrees>{3}</LatitudeDegrees>
          <LongitudeDegrees>{4}</LongitudeDegrees>
        </BeginPosition>
        <EndPosition>
          <LatitudeDegrees>{5}</LatitudeDegrees>
          <LongitudeDegrees>{6}</LongitudeDegrees>
        </EndPosition>
        <AverageHeartRateBpm xsi:type=""HeartRateInBeatsPerMinute_t"">
          <Value>{7}</Value>
        </AverageHeartRateBpm>
        <MaximumHeartRateBpm xsi:type=""HeartRateInBeatsPerMinute_t"">
          <Value>{8}</Value>
        </MaximumHeartRateBpm>
        <Intensity>Active</Intensity>
      </Lap>
      <Track>
", Name,
              (ed.Time - st.Time).TotalSeconds,
              0, //DistanceMeters, RunKeeper不需,省略
              st.LatitudeDegrees, st.LongitudeDegrees,
              ed.LatitudeDegrees, ed.LongitudeDegrees,
              0, //AverageHeartRateBpm, RunKeeper不需,省略
              TrackPoints.Select(o => o.HeartRateBpm).Max() //MaximumHeartRateBpm
              );
            foreach (TrackPoint tp in TrackPoints)
                sb.AppendLine(tp.ToXml());
            sb.AppendLine(@"     </Track>
    </Course>
  </Courses>
</TrainingCenterDatabase>");
            return sb.ToString();
        }
    }
 
    public class TrackPoint
    {
        public DateTime Time;
        public decimal LatitudeDegrees;
        public decimal LongitudeDegrees;
        public decimal AltitudeMeters;
        public decimal DistanceMeters;
        public decimal HeartRateBpm;
        public string ToXml()
        {
            return string.Format(@"<Trackpoint>
          <Time>{0:yyyy-MM-ddTHH:mm:ssZ}</Time>
          <Position>
            <LatitudeDegrees>{1}</LatitudeDegrees>
            <LongitudeDegrees>{2}</LongitudeDegrees>
          </Position>
          <AltitudeMeters>{3}</AltitudeMeters>
          <DistanceMeters>{4}</DistanceMeters>
          <HeartRateBpm xsi:type=""HeartRateInBeatsPerMinute_t"">
            <Value>{5}</Value>
          </HeartRateBpm>
          <SensorState>Absent</SensorState>
        </Trackpoint>", Time, LatitudeDegrees, LongitudeDegrees,
        AltitudeMeters, DistanceMeters, HeartRateBpm);
        }
    }
}

就這樣,我的GH-625M記錄也能上傳RunKeeper囉! 會寫程式真好~~

歡迎推文分享:
Published 24 April 2012 07:50 AM 由 Jeffrey
Filed under: ,
Views: 31,288



意見

# Jason said on 06 August, 2012 10:07 PM

您好,最近也入手了625M,也有想要把資料輸出到Endomondo,google了一下看到您的文章,不曉得您是否可以把這個轉檔軟體分享出來造福大眾呢?如不方便沒問題,還是感謝。

# Jeffrey said on 17 August, 2012 10:20 AM

to Jason, 我做了一個ACT->TCX的線上服務,請享用: blog.darkthread.net/post-2012-08-17-act2tcx-online.aspx

# MJ said on 07 September, 2013 08:43 PM

Jeffery

tks. I use 625 also. And your software helps a lot.

你的看法呢?

(必要的) 
(必要的) 
(選擇性的)
(必要的) 
(提醒: 因快取機制,您的留言幾分鐘後才會顯示在網站,請耐心稍候)

5 + 3 =

搜尋

Go

<April 2012>
SunMonTueWedThuFriSat
25262728293031
1234567
891011121314
15161718192021
22232425262728
293012345
 
RSS
創用 CC 授權條款
【廣告】
twMVC
最新回應

Tags 分類檢視
關於作者

一個醉心技術又酷愛分享的Coding魔人,十年的IT職場生涯,寫過系統、管過專案, 也帶過團隊,最後還是無怨無悔地選擇了技術鑽研這條路,近年來則以做一個"有為的中年人"自許。

文章典藏
其他功能

這個部落格


Syndication