RINEX广播星历文件读取(N文件)

卫星星历是描述卫星轨道运动的一组参数,根据这些参数可以计算出卫星在任意适合的位置及其运动速度。有了卫星的位置,加上接收机的观测值,就可以对接收机的位置进行求解。

本文首先对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

右侧日期范围需要键盘输入手动调整,观测值文件的下载需要选择测站,点击根据日期搜索即可。

RINEX广播星历文件读取(N文件)_第1张图片

需要注意的是,初学者最好选择RINEX2.1.1版本的单系统的观测值文件(15年以前大多都是),普通txt形式打开文件会造成格式错误,需要以notepad++或者vs编译器打开。

广播星历N文件格式解读

广播星历包包含了卫星一系列参数和必要的摄动改正数。当前的卫星轨道参数是根据前一段时间求出的轨道参数外推得到的,因此广播星历也叫预报星历。

N文件示例:

RINEX广播星历文件读取(N文件)_第2张图片

N文件头

以下是用notepad++打开的N文件。

RINEX广播星历文件读取(N文件)_第3张图片

左边为信息,右侧为对应的标签(第60个字符) ,记住这个60,在后续读取数据中会用到。

第一行:记录了RINEX的版本号和观测类型

第二行:创建本数据文件所采用的:程序名称、单位名称及日期

第三行:注释行

第四行:历书中电离层参数:A0~A4

第五行:历书中电离层参数:B0~B3(第五行第六行的参数可做电离层改正)

第六行:用于计算UTC时间的历书参数;A0,A1为多项式系数;T为UTC数据的参考时刻;W为UTC参考周数,为连续计数

第七行:跳秒,GPS时与UTC时之差

第八行:"END OF HEADER"头文件的结束标志

在进行伪距程序设计时,N文件只需要读取头尾两行的内容即可。

N文件数据块

RINEX广播星历文件读取(N文件)_第4张图片

从第九行开始,记录了每颗卫星的参数信息,八行为一个数据块,卫星之间的信息没有空行。

第一行第一列代表了卫星的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文件程序设计

读取N文件主要需要C语言中的文件操作、动态内存管理(malloc)以及指针等。

结构体声明

创建N文件头结构体

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重命名后,在创建结构体变量或结构体指针时,用下列等式右边的即可: 

  • struct nav_head  =  nav_head
  • sturct nav_head* =  pnav_head

 后续有关结构体声明也是如此。

创建N文件数据块结构体

按照上述所讲的参数以及类型,依次创建结构体成员,除了卫星的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;

创建结构体成员时,最好按照上文所给的官方命名进行声明,增加代码的可读性。

N文件数据读取

文件读取的思路是:

  • 创建文件指针,打开文件
  • 读取文件一共有多少行,计算N文件有多少个卫星的数据块(读取完行数后需要将文件指针返回初始位置)
  • 分别创建N文件头和N文件数据块的指针,并为其开辟内存。
  • 将数据依次按行进行读取
  • 关闭文件

具体代码示例如下:

//数据读取
	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文件

上述代码中,getrowreadrinex_n是我们需要创建的函数,其中readrinex_n中还包含一个将字符串转换成double类型的函数strtonum,这也是需要自行创建的,下面分别对其进行介绍。

获取文件行数(getrow)

用fgets函数逐行对文件进行读取:

fegts函数需要包含头文件,使用方法如下:

  • char * fgets ( char * str, int num, FILE * stream );
  • 输入一个char*类型的指针(用来存放读取的数据),读取的个数,文件指针,其返回类型为char* 也就是读取后的新位置。

以头文件“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用来查找标签。

字符串转换成浮点数(strtonum)

在用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部分是用来判断人为传入参数时所引起错误:

  •     起始位置<0
  •     读取字符串个数
  •     str里面存放的字节数

(当然,上面这些傻瓜式错误相信大多数人都不会犯的,而且程序员真想写bug怎么也拦不住。但是rtklib库里是有这么一个判断,还是相信大佬的智慧吧)

关于科学计数法:

仔细观察N文件会发现:参数都是以科学计数法的方式来存储的(D或者d(老版本会有d)),而C语言中科学计数法的标志是‘e’,在读取时还需注意这一点。

例如:10的科学计数法在文本中存储的形式是1.0D-1,需要把它转换成1.0e-1。

明白上面的例子后,再来看这个三目操作符就很简单了:

*p++ = ((*buff == 'D' || *buff == 'd') ? 'e' : *buff);

  • (*buff == 'D' || *buff == 'd')是判断部分,当二者有一个为真是则执行‘e’,都为假时则执行*buff(该穿数字传数字,该传空格穿空格)
  • 将右侧三目操作符的结果赋值给*P
  • 赋值完成后,p++,指针向后移动一位,继续一下参数的转换

 格式化转换

当对科学计数法处理完后,利用sscanf可以将其格式化输入到我们创建的变量value中,再将其作为返回值返回。

sscanf(str, "%lf", &value) == 1 ? value : 0.0;

sscanf函数的用法如下:

  • 包含头文件
  • int sscanf( const char *buffer, const char *format [, argument ] ... );
  • 需要传入的参数分别为:源头、形式、目的地
  • 返回值是传入参数的个数(或项),失败则返回EOF

根据返回值再对其做一个三目操作符的判断,如果为1则说明传入成功,不是1则返回0。

数据读取

当前期的准备工作完成后,后期的读取工作则非常简单了,主要思想如下:

  • 按卫星数据块为单位,for循环进行读取
  • 每个数据块利用switch语句按行进行读取
  • 读取的时需要对标N文件,找到数据对应的格式

相应代码如下:

//读取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';
	}
}
  • des为拷贝的目标,src为拷贝的源头,n为拷贝的个数
  • 先将所有字符串拷贝,然后再从尾部开始判断,去掉空格

将上述函数在头文件中进行声明,而后在主函数中调用即可,为了防止出错,一定要养成写一步调试一步的习惯,也要养成写注释的好习惯。不同版本的观测值文件的格式会有所不同,但广播星历文件的格式都是大致一样的。

你可能感兴趣的:(卫星导航定位,c语言,开发语言,大数据)