卫星星历是描述卫星轨道运动的一组参数,根据这些参数可以计算出卫星在任意适合的位置及其运动速度。有了卫星的位置,加上接收机的观测值,就可以对接收机的位置进行求解。
本文首先对RINEX广播星历进行格式介绍,然后基于C语言,以程序设计的角度讲解如何读取数据,每行代码皆有详细注释和讲解,希望可以为测绘学子们带来帮助。
观测值文件的数据读取,可以查看观测值文件的数据读取
目录
下载数据
广播星历N文件格式解读
N文件头
N文件数据块
读取N文件程序设计
结构体声明
创建N文件头结构体
创建N文件数据块结构体
N文件数据读取
获取文件行数(getrow)
字符串转换成浮点数(strtonum)
数据读取
武汉大学IGS数据中心可以下载观测值文件、广播星历和精密星历。(最近一年的可能没有)
武汉大学IGS数据中心 (gnsswhu.cn)http://www.igs.gnsswhu.cn/index.php/Home/DataProduct/igs.html
右侧日期范围需要键盘输入手动调整,观测值文件的下载需要选择测站,点击根据日期搜索即可。
需要注意的是,初学者最好选择RINEX2.1.1版本的单系统的观测值文件(15年以前大多都是),普通txt形式打开文件会造成格式错误,需要以notepad++或者vs编译器打开。
广播星历包包含了卫星一系列参数和必要的摄动改正数。当前的卫星轨道参数是根据前一段时间求出的轨道参数外推得到的,因此广播星历也叫预报星历。
N文件示例:
以下是用notepad++打开的N文件。
左边为信息,右侧为对应的标签(第60个字符) ,记住这个60,在后续读取数据中会用到。
第一行:记录了RINEX的版本号和观测类型
第二行:创建本数据文件所采用的:程序名称、单位名称及日期
第三行:注释行
第四行:历书中电离层参数:A0~A4
第五行:历书中电离层参数:B0~B3(第五行第六行的参数可做电离层改正)
第六行:用于计算UTC时间的历书参数;A0,A1为多项式系数;T为UTC数据的参考时刻;W为UTC参考周数,为连续计数
第七行:跳秒,GPS时与UTC时之差
第八行:"END OF HEADER"头文件的结束标志
在进行伪距程序设计时,N文件只需要读取头尾两行的内容即可。
从第九行开始,记录了每颗卫星的参数信息,八行为一个数据块,卫星之间的信息没有空行。
第一行第一列代表了卫星的PRN号,之后八行四列的参数意义如下:
卫星钟时间(toc时刻,年月日时分秒) 卫星钟差(a0,s) 卫星钟偏(a1,s/s) 卫星钟偏移(a2,s/s²)
数据龄期(AODE) 轨道半径改正项(Crs,rad) 平均角速度改正项(deltan) 平近点角(M0,rad)
升交点角距改正项(Cuc,rad) 轨道偏心率(e) 升交点角距改正项(Cus,rad) 轨道长半轴平方根(sqrtA)
星历的参考时刻(TOE) 轨道倾角的改正项(Cic,rad) 升交点经度(OMEGA) 轨道倾角改正项(Cis,rad)
轨道倾角(i0) 轨道半径的改正项(Crc,m) 近地点角距(omega,rad) 升交点赤经变化(deltaomega,rad)
轨道倾角的变率(IDOT) L2频道C/A码标识 GPS时间周(GPS Week) L2P码标识
卫星精度(SVA,m) 卫星健康(SVH) 电离层延迟(TGD,s) 星钟的数据质量(IODC)
信息发射时间 空 空 空
以上是导航星历数据块的全部参数,伪距单点定位读取到第六行第二列IDOT即可。
读取N文件主要需要C语言中的文件操作、动态内存管理(malloc)以及指针等。
N文件头只需要存储第一行的信息,比较简单。为了之后调用方便,在创建结构体时,最好用typedef对结构体进行重命名。
/导航头文件结构体
typedef struct nav_head
{
double ver;//rinex 版本号
char type[20];//读取的数据类型
double ION_ALPHA[4];//8个电离层参数
double ION_BETA[4];
double DELTA_UTC[4];
int leap;
}nav_head, *pnav_head;
用typedef重命名后,在创建结构体变量或结构体指针时,用下列等式右边的即可:
后续有关结构体声明也是如此。
按照上述所讲的参数以及类型,依次创建结构体成员,除了卫星的PRN号、年、月、日、时、分(秒是double类型的)以外,其他参数均为double类型,为了更加直观,建议在创建结构体时按行进行创建(这里不考虑结构体内存对齐)。
//导航数据结构体
typedef struct nav_body
{
//数据块第一行内容:
int sPRN;//卫星PRN号
//历元:TOC中卫星钟的参考时刻
int TOC_Y;//年
int TOC_M;//月
int TOC_D;//日
int TOC_H;//时
int TOC_Min;//分
int TOC_Sec;//秒
double sa0;//卫星钟差
double sa1;//卫星钟偏
double sa2;//卫星钟漂
//数据块第二行内容:
double IODE;//数据、星历发布时间(数据期龄)
double Crs;//轨道半径的正弦调和改正项的振幅(单位:m)
double deltan;//卫星平均运动速率与计算值之差(rad/s)
double M0;//参考时间的平近点角(rad)
//数据块第三行内容:
double Cuc;//维度幅角的余弦调和改正项的振幅(rad)
double e;//轨道偏心率
double Cus;//轨道幅角的正弦调和改正项的振幅(rad)
double sqrtA;//长半轴平方根
//数据块第四行内容:
double TOE;//星历的参考时刻(GPS周内秒)
double Cic;//轨道倾角的余弦调和改正项的振幅(rad)
double OMEGA;//参考时刻的升交点赤经
double Cis;//维度倾角的正弦调和改正项的振幅(rad)
//数据块第五行内容:
double i0;//参考时间的轨道倾角(rad)
double Crc;//轨道平径的余弦调和改正项的振幅(m)
double omega;//近地点角距
double deltaomega;//升交点赤经变化率(rad)
//数据块第六行内容:
double IDOT;//近地点角距(rad/s)
double L2code;//L2上的码
double GPSweek;//GPS周,于TOE一同表示
double L2Pflag;//L2,p码数据标记
//数据块第七行内容
double sACC;//卫星精度
double sHEA;//卫星健康状态
double TGD;//sec
double IODC;//钟的数据龄期
//数据块第八行内容
double TTN;//电文发送时间
double fit;//拟合区间
double spare1;//空
double spare2;//空
}nav_body, *pnav_body;
创建结构体成员时,最好按照上文所给的官方命名进行声明,增加代码的可读性。
文件读取的思路是:
具体代码示例如下:
//数据读取
FILE* fp_nav = NULL;//导航星历文件指针
FILE* fp_obs = NULL;//观测值文件指针
pnav_head nav_h = NULL;
pnav_body nav_b = NULL;
//N文件读取
fp_nav = fopen("abpo0480.15n", "r");//以只读的方式打开N文件
int n_n = getrow(fp_nav);//获取导航文件观测行数
rewind(fp_nav);//将文件指针返回值起始位置
nav_h = (pnav_head)malloc(sizeof(nav_head));//给N文件头开辟空间
nav_b = (pnav_body)malloc(sizeof(nav_body) * (n_n / 8));
if (nav_h && nav_b)
{
readrinex_n(fp_nav, nav_h, nav_b, n_n);//读取数据
}
fclose(fp_nav);//关闭N文件
上述代码中,getrow和readrinex_n是我们需要创建的函数,其中readrinex_n中还包含一个将字符串转换成double类型的函数strtonum,这也是需要自行创建的,下面分别对其进行介绍。
用fgets函数逐行对文件进行读取:
fegts函数需要包含头文件
以头文件“END OF HEADER”为标志,下一行开始计数,读取到的行数除以8为N文件中数据块的数量。
//获取文件数据块行数,从END OF HEADER后开始起算
extern int getrow(FILE* fp_nav)
{
int row = 0;
int flag = 0;
char buff[MAXRINEX] = { 0 };//用来存放读取到的字符串
char* lable = buff + 60;
//gets函数,读取一行,当读取结束后返回NULL指针,格式如下:
//char * fgets ( char * str, int num, FILE * stream );
while (fgets(buff, MAXRINEX, fp_nav))
{
//strstr:查找字符串中的指定字符或字符串,格式如下:
//const char * strstr ( const char * str1, const char * str2 );
if (flag == 1)
{
row++;
continue;
}
if (strstr(lable, "END OF HEADER"))
{
flag = 1;
}
}
return row;
}
row为最后要返回的行数,falg为判断标志,当读取到文件头结束标签“END OF HEADER”时,flag=1,row开始计数,char* lable +60用来查找标签。
在用fgets函数读取数据时,我们是创建了一个buff的字符串接收的,对于正好是字符串类型的信息,例如观测类型tpye,我们可以利用strncpy函数(需包含头文件
但是很多参数是int、double类型的,那么我们需要先将其从char类型转换成double类型的,再对结构体成员进行赋值。
//将字符串转换为浮点数,i起始位置,n输入多少个字符
static double strtonum(const char* buff, int i, int n)
{
double value = 0.0;
char str[256] = { 0 };
char* p = str;
/************************************
* 当出现以下三种情况报错,返回0.0
* 1.起始位置<0
* 2.读取字符串个数= 0; buff++)
{
//三目操作符:D和d为文件中科学计数法部分,将其转换成二进制能读懂的e
*p++ = ((*buff == 'D' || *buff == 'd') ? 'e' : *buff);
}
*p = '\0';
//三目操作符,将str中存放的数以格式化读取到value中。
return sscanf(str, "%lf", &value) == 1 ? value : 0.0;
}
判断傻瓜错误:
上述代码中,if部分是用来判断人为传入参数时所引起错误:
(当然,上面这些傻瓜式错误相信大多数人都不会犯的,而且程序员真想写bug怎么也拦不住。但是rtklib库里是有这么一个判断,还是相信大佬的智慧吧)
关于科学计数法:
仔细观察N文件会发现:参数都是以科学计数法的方式来存储的(D或者d(老版本会有d)),而C语言中科学计数法的标志是‘e’,在读取时还需注意这一点。
例如:10的科学计数法在文本中存储的形式是1.0D-1,需要把它转换成1.0e-1。
明白上面的例子后,再来看这个三目操作符就很简单了:
*p++ = ((*buff == 'D' || *buff == 'd') ? 'e' : *buff);
格式化转换
当对科学计数法处理完后,利用sscanf可以将其格式化输入到我们创建的变量value中,再将其作为返回值返回。
sscanf(str, "%lf", &value) == 1 ? value : 0.0;
sscanf函数的用法如下:
根据返回值再对其做一个三目操作符的判断,如果为1则说明传入成功,不是1则返回0。
当前期的准备工作完成后,后期的读取工作则非常简单了,主要思想如下:
相应代码如下:
//读取N文件
extern void readrinex_n(FILE* fp_nav, pnav_head nav_h, pnav_body nav_b, int n_n)
{
char buff[MAXRINEX] = { 0 };
char* lable = buff + 60;
int i = 0;
int j = 0;
while (fgets(buff, MAXRINEX, fp_nav))
{
if (strstr(lable, "RINEX VERSION / TYPE"))
{
nav_h->ver = strtonum(buff, 0, 9);
strncpy((nav_h->type), buff + 20, 15);
continue;
}
else if (strstr(lable, "ION ALPHA"))
{
nav_h->ION_ALPHA[0] = strtonum(buff, 3, 12);
nav_h->ION_ALPHA[1] = strtonum(buff, 3 + 12, 12);
nav_h->ION_ALPHA[2] = strtonum(buff, 3 + 12 + 12, 12);
nav_h->ION_ALPHA[3] = strtonum(buff, 3 + 12 + 12 + 12, 12);
continue;
}
else if (strstr(lable, "ION BETA"))
{
nav_h->ION_BETA[0] = strtonum(buff, 3, 12);
nav_h->ION_BETA[1] = strtonum(buff, 3+12, 12);
nav_h->ION_BETA[2] = strtonum(buff, 3+12+12, 12);
nav_h->ION_BETA[3] = strtonum(buff, 3+12+12+12, 12);
continue;
}
else if (strstr(lable, "DELTA-UTC: A0,A1,T,W"))
{
nav_h->DELTA_UTC[0] = strtonum(buff, 3, 19);
nav_h->DELTA_UTC[1] = strtonum(buff, 3+19, 19);
nav_h->DELTA_UTC[2] = strtonum(buff, 3+19+19, 9);
nav_h->DELTA_UTC[3] = strtonum(buff, 3+19+19+9, 9);
continue;
}
else if (strstr(lable, "LEAP SECONDS"))
{
nav_h->leap = (int)strtonum(buff, 6, 2);
}
else if (strstr(lable, "END OF HEADER"))//这时开始对数据进行读取
{
for (i = 0; i < (n_n / 8); i++)//n_n为不包含头的行数,除以8为数据块数量
{
for (j = 0; j < 8; j++)
{
fgets(buff, MAXRINEX, fp_nav);
switch (j)
{
case 0:
nav_b[i].sPRN = (int)strtonum(buff, 0, 2);
nav_b[i].TOC_Y = (int)strtonum(buff, 3, 2) + 2000;
nav_b[i].TOC_M = (int)strtonum(buff, 6, 2);
nav_b[i].TOC_D = (int)strtonum(buff, 9, 2);
nav_b[i].TOC_H = (int)strtonum(buff, 12, 2);
nav_b[i].TOC_Min = (int)strtonum(buff, 15, 2);
nav_b[i].TOC_Sec = strtonum(buff, 18, 2);
nav_b[i].sa0 = strtonum(buff, 22, 19);
nav_b[i].sa1 = strtonum(buff, 22 + 19, 19);
nav_b[i].sa2 = strtonum(buff, 22 + 19 + 19, 19);
break;
case 1:
nav_b[i].IODE = strtonum(buff, 3, 19);
nav_b[i].Crs = strtonum(buff, 3 + 19, 19);
nav_b[i].deltan = strtonum(buff, 3 + 19 + 19, 19);
nav_b[i].M0 = strtonum(buff, 3 + 19 + 19 + 19, 19);
break;
case 2:
nav_b[i].Cuc = strtonum(buff, 3, 19);
nav_b[i].e = strtonum(buff, 3 + 19, 19);
nav_b[i].Cus = strtonum(buff, 3 + 19 + 19, 19);
nav_b[i].sqrtA = strtonum(buff, 3 + 19 + 19 + 19, 19);
break;
case 3:
nav_b[i].TOE = strtonum(buff, 3, 19);
nav_b[i].Cic = strtonum(buff, 3 + 19, 19);
nav_b[i].OMEGA = strtonum(buff, 3 + 19 + 19, 19);
nav_b[i].Cis = strtonum(buff, 3 + 19 + 19 + 19, 19);
break;
case 4:
nav_b[i].i0 = strtonum(buff, 3, 19);
nav_b[i].Crc = strtonum(buff, 3 + 19, 19);
nav_b[i].omega = strtonum(buff, 3 + 19 + 19, 19);
nav_b[i].deltaomega = strtonum(buff, 3 + 19 + 19 + 19, 19);
break;
case 5:
nav_b[i].IDOT = strtonum(buff, 3, 19);
nav_b[i].L2code = strtonum(buff, 3 + 19, 19);
nav_b[i].GPSweek= strtonum(buff, 3 + 19 + 19, 19);
nav_b[i].L2Pflag = strtonum(buff, 3 + 19 + 19 + 19, 19);
break;
case 6:
nav_b[i].sACC = strtonum(buff, 3, 19);
nav_b[i].sHEA = strtonum(buff, 3 + 19, 19);
nav_b[i].TGD = strtonum(buff, 3 + 19 + 19, 19);
nav_b[i].IODC = strtonum(buff, 3 + 19 + 19 + 19, 19);
break;
case 7:
nav_b[i].TTN = strtonum(buff, 3, 19);
nav_b[i].fit = strtonum(buff, 3 + 19, 19);
nav_b[i].spare1 = strtonum(buff, 3 + 19 + 19, 19);
nav_b[i].spare2 = strtonum(buff, 3 + 19 + 19 + 19, 19);
break;
}
}
}
}
}
}
读取的过程比较冗长,主要是需要传入的参数太多了。
需要注意的是:每次case的情况完成后需要加入break,防止程序依次进入下一个case。
补充:
后来发现strncpy不会自动补’\0’ ,在后续想输出字符串时会遇到一些问题,模拟实现一个strncpy函数即可,代码如下(参考rtklib)
void setstr(char* des, const char* src, int n)
{
char* p = des;
const char* q = src;
while (*q && q < src+n)
{
*p++ = *q++;
}
*p-- = '\0';
//去掉尾部空格
while (p >= des && *p == ' ')
{
*p-- = '\0';
}
}
将上述函数在头文件中进行声明,而后在主函数中调用即可,为了防止出错,一定要养成写一步调试一步的习惯,也要养成写注释的好习惯。不同版本的观测值文件的格式会有所不同,但广播星历文件的格式都是大致一样的。