GPX全称GPS Exchange Format,是基于XML的通用导航轨迹数据格式,常用来存储GPS设备记录的轨迹点、路线、航点等信息。在C++项目中解析GPX文件,核心是对XML结构进行遍历,定位到存储轨迹数据的节点后提取对应的坐标和属性值。

解析前的环境准备
本文使用libxml2库实现XML解析,这是C语言编写的开源XML处理库,C++项目可以直接调用。首先需要安装libxml2开发包,Ubuntu系统可以通过apt命令安装:
sudo apt-get install libxml2-dev
如果是Windows环境,可以从libxml2官网下载编译好的库文件,配置到项目的包含目录和库目录中即可。
GPX文件结构说明
标准的GPX轨迹文件结构如下,核心的轨迹数据存储在<trk>(轨迹)下的<trkseg>(轨迹段)中的<trkpt>(轨迹点)节点里:
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="Example">
<trk>
<name>测试轨迹</name>
<trkseg>
<trkpt lat="39.908823" lon="116.397470">
<ele>50.2</ele>
<time>2024-05-20T08:30:00Z</time>
</trkpt>
<trkpt lat="39.908900" lon="116.397500">
<ele>51.0</ele>
<time>2024-05-20T08:30:10Z</time>
</trkpt>
</trkseg>
</trk>
</gpx>
其中<trkpt>的lat和lon属性就是经纬度坐标,<ele>子节点是海拔,<time>子节点是时间戳,这些都是我们通常需要提取的核心数据。
XML节点遍历与坐标提取实现
定义轨迹点数据结构
首先定义存储单个轨迹点信息的结构体,方便后续数据管理:
#include <string>
#include <vector>
// 轨迹点结构体
struct TrackPoint {
double latitude; // 纬度
double longitude; // 经度
double elevation; // 海拔
std::string time; // 时间戳
};
// 轨迹段结构体
struct TrackSegment {
std::vector<TrackPoint> points;
};
// 完整轨迹结构体
struct Track {
std::string name;
std::vector<TrackSegment> segments;
};
核心解析代码实现
下面是完整的GPX解析函数,包含XML文档加载、节点遍历、坐标提取的完整逻辑:
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <iostream>
#include <vector>
// 解析单个轨迹点
TrackPoint parseTrackPoint(xmlNodePtr trkptNode) {
TrackPoint point;
// 提取lat和lon属性
xmlAttrPtr attr = trkptNode->properties;
while (attr != NULL) {
std::string attrName = (char*)attr->name;
if (attrName == "lat") {
point.latitude = atof((char*)xmlGetProp(trkptNode, attr->name));
} else if (attrName == "lon") {
point.longitude = atof((char*)xmlGetProp(trkptNode, attr->name));
}
attr = attr->next;
}
// 遍历子节点提取ele和time
xmlNodePtr child = trkptNode->children;
while (child != NULL) {
std::string nodeName = (char*)child->name;
if (nodeName == "ele" && child->children != NULL) {
point.elevation = atof((char*)child->children->content);
} else if (nodeName == "time" && child->children != NULL) {
point.time = (char*)child->children->content;
}
child = child->next;
}
return point;
}
// 解析GPX文件返回所有轨迹
std::vector<Track> parseGPXFile(const char* filePath) {
std::vector<Track> tracks;
// 加载XML文档
xmlDocPtr doc = xmlReadFile(filePath, NULL, 0);
if (doc == NULL) {
std::cerr << "无法加载GPX文件: " << filePath << std::endl;
return tracks;
}
// 获取根节点gpx
xmlNodePtr root = xmlDocGetRootElement(doc);
if (root == NULL) {
std::cerr << "GPX文件根节点为空" << std::endl;
xmlFreeDoc(doc);
return tracks;
}
// 遍历根节点下的所有子节点
xmlNodePtr child = root->children;
while (child != NULL) {
std::string nodeName = (char*)child->name;
if (nodeName == "trk") { // 找到轨迹节点
Track track;
// 遍历轨迹节点下的子节点
xmlNodePtr trkChild = child->children;
while (trkChild != NULL) {
std::string trkChildName = (char*)trkChild->name;
if (trkChildName == "name" && trkChild->children != NULL) {
track.name = (char*)trkChild->children->content;
} else if (trkChildName == "trkseg") { // 找到轨迹段节点
TrackSegment segment;
// 遍历轨迹段下的轨迹点
xmlNodePtr trkptNode = trkChild->children;
while (trkptNode != NULL) {
std::string ptName = (char*)trkptNode->name;
if (ptName == "trkpt") {
segment.points.push_back(parseTrackPoint(trkptNode));
}
trkptNode = trkptNode->next;
}
track.segments.push_back(segment);
}
trkChild = trkChild->next;
}
tracks.push_back(track);
}
child = child->next;
}
// 释放XML文档资源
xmlFreeDoc(doc);
// 清理libxml2全局资源
xmlCleanupParser();
return tracks;
}
调用示例
下面是调用上述解析函数的示例,演示如何输出提取到的轨迹坐标:
int main() {
std::vector<Track> tracks = parseGPXFile("test.gpx");
for (size_t i = 0; i < tracks.size(); i++) {
std::cout << "轨迹名称: " << tracks[i].name << std::endl;
for (size_t j = 0; j < tracks[i].segments.size(); j++) {
std::cout << "轨迹段 " << j+1 << " 包含 " << tracks[i].segments[j].points.size() << " 个轨迹点" << std::endl;
for (size_t k = 0; k < tracks[i].segments[j].points.size(); k++) {
TrackPoint pt = tracks[i].segments[j].points[k];
std::cout << "点" << k+1 << ": 纬度=" << pt.latitude << ", 经度=" << pt.longitude
<< ", 海拔=" << pt.elevation << ", 时间=" << pt.time << std::endl;
}
}
}
return 0;
}
编译与注意事项
编译时需要链接libxml2库,编译命令如下:
g++ -o gpx_parser main.cpp -lxml2
解析过程中需要注意几个问题:一是GPX文件可能有多个<trk>节点,对应多条轨迹;二是部分轨迹点可能没有<ele>或<time>子节点,需要做空值判断避免程序崩溃;三是libxml2的函数返回的内容需要正确转换类型,字符串内容需要转换为char*后再处理。
常见问题处理
如果遇到解析失败的情况,可以先检查GPX文件编码是否为UTF-8,libxml2默认支持UTF-8编码。如果文件包含命名空间,遍历节点时需要处理命名空间前缀,比如节点名可能显示为gpxtrk:trk,需要匹配节点名后缀。另外如果轨迹数据量很大,可以优化节点遍历逻辑,减少不必要的节点判断,提升解析效率。