数据描述记录[DDR]
DDR的字段区(Field area)包含了各字段的相关信息,每一字段包含相应标签,以单元终止符(UT)为分隔的控制内容,最后以字段终止符(FT)作为字段的结束标志。通过查询S-57附录中的产品说明,即可知道字段各标签的具体含义。
字段间并不是扁平结构,而是存在上下级关系的树状结构,被存储在0000
标签里,也被称为字段控制字段
,树状结构的根节点为0001
,是被S-57文件强制规定,表示该文件满足ISO/IEC 8211编码格式。而树状结构中每一节点的描述信息,则被存储在数据描述字段
里。
字段控制字段
字段控制字段由0000标签开始,接下为五个字符;&□□□的控制部分,然后以UT分隔,接着是字段树状结构对信息,并以FT作为结束标志。
字段标识 0000 | ;&□□□ | UT | 字段的树状结构对 | FT |
---|
通过二进制解析实际文件,可清晰了解其字段控制字段的内容:
由图中可知该文件字段结构对为:0001->DSID, DSID->DSSI, 0001->DSPM, 0001->VRID, VRID->ATTV, VRID->VRPT, VRID->SG2D, VRID->SG3D, 0001->FRID, FRID->FOID, FRID->ATTF, FRID->NATF, FRID->FFPT, FRID->FSPT。该结构对可还原为如下树状结构:
了解了字段控制字段的数据格式后,新建类S57Tag
类存储树状图:
public class S57Tag
{
public string Tag;
public S57Tag Parent;
public List<S57Tag> Children;
}
新建S57FieldControlField
用以解析字段控制字段:
public class S57FieldControlField
{
//根Tag为0001
public S57Tag Tag { get; set; } = new S57Tag { Tag = "0001" };
/// <summary>
/// 字段控制字段的构造函数
/// </summary>
/// <param name="fieldControlSize">该字段的大小</param>
/// <param name="tagSize">标识所占用的字符数,一般为4</param>
public S57FieldControlField(int fieldControlSize, int tagSize)
{
// 初始的当前节点为根节点
var curr = Tag;
var controlField = BytesHelper.GetString(fieldControlSize);
if (string.IsNullOrWhiteSpace(controlField)) return;
// 字段区由0000开始,到第一个单元终止符结束
var tag0000 = controlField.Substring(0, tagSize);
var firstUT = controlField.IndexOf(Helper.UT);
// 一次取一对字段标识
for (int i = firstUT + 1; i < controlField.Length - tagSize * 2; i += tagSize * 2)
{
// 前tagSize个字符为父节点
var tagParent = controlField.Substring(i, tagSize);
// 后tagSize个字符为子节点
var tagChild = controlField.Substring(i + tagSize, tagSize);
// 当前节点与父节点不同时,往上回溯直到当前节点为父节点
while (curr.Tag != tagParent)
{
if (curr.Parent == null) break;
curr = curr.Parent;
}
// 当前节点为父节点时,添加相应子节点,并将子节点设为当前节点
if (tagParent == curr.Tag)
{
var child = new S57Tag() { Tag = tagChild };
child.Parent = curr;
if (curr.Children == null) curr.Children = new List<S57Tag>();
curr.Children.Add(child);
curr = child;
}
}
}
}
添加验证代码如下:
// 利用递归显示树状图
public static void DisplayTag(S57Tag tag, int level)
{
if (level == 0)
{
Console.WriteLine(tag.Tag);
}
else
{
Console.WriteLine("".PadLeft(level*2) + "|-" + tag.Tag);
}
if(tag.Children != null && tag.Children.Count > 0)
{
foreach (var child in tag.Children)
{
DisplayTag(child, level + 1);
}
}
}
......
var tag0000 = dir.Items[0];
var fcf = new S57FieldControlField(tag0000.FieldLength, dl.FieldTagSize);
DisplayTag(fcf.Tag, 0);
由验证结果可知,字段控制字段部分的代码解析正确。
数据描述字段
数据描述字段由字段控制、字段名、属性列表及格式控制列表四部分组成,并以FT作为结束标志,对应类S57DataDescriptiveField
。全部数据描述字段对应类S57DataDescriptiveFields
。
字段控制说明 | 字段名 | UT | 属性列表 | UT | 格式控制列表 | FT |
---|
-
字段控制说明由数据描述字段定义的数据字段的级别和数据类型,对应类
S57FieldControl
,其结构如下:起始位 长度 项目名 内容 示例中内容 0 1 数据结构代码 “0”:单数据项
“1”:线性结构
“2”:多维结构“1” 1 1 数据类型代码 “0”:字符串
“1”:整型
“5”:二进制形式
“6”:混合类型
“6” 2 2 辅助控制码 “00” “00” 4 2 可打印字符 “;&” “;&” 6 3 截取转义序列 词汇级别0:□□□
词汇级别1:-A□
词汇级别2:%/A"□□□" 不同的词汇级别,对应的UT和FT的长度不一样。
字段名是字段标签相对应的名称,对应类中属性``````,示例中值为:
Data set identification field
。-
属性列表:字符所包含的属性的列表,各属性之间用“!”分隔;对于
0001
标签,属性列表为空,但默认存在一个RCID属性。示例中值为:
RCNM!RCID!EXPP!INTU!DSNM!EDTN!UPDN!UADT!ISDT!STED!PRSP!PSDN!PRED!PROF!AGEN!COMT,表示DSID标签中包含16个属性,每一四字符属性所表达的具体含义见S-57产品说明。
若属性为重复属性(如地理位置中的经纬度),则以*
号表示(*YCOO!XCOO) -
格式控制列表:存储属性列表相对应的格式,即字段存在多少属性,每一属性就有其相应的格式作为解析二进制文件的依据。
示例中值为:
(b11,b14,2b11,3A,2A(8),R(4),b11,2A,b11,b12,A),括号中格式依次为1个b11,1个b14,2个b11,3个A,2个A(8),1个R(4),1个b11,2个A,1个b11,1个b12,1个A,正好16个格式,与DSID的16个属性一一对应。
仿照数据描述字段的结构,新建如下类:
// 数据描述字段区
public class S57DataDescriptiveFields
{
public List<S57DataDescriptiveField> Fields; //所有描述字段
}
// 数据描述字段
public class S57DataDescriptiveField
{
public string Tag; //字段标签
public S57FieldControl FieldControl; //字段控制说明
public string FieldName = “”; //字段名称
public List<S57FieldAttrFormat> FieldAttrFormats; //字段属性格式列表
}
// 字段控制说明
public class S57FieldControl
{
public string DataStructureCode; //数据结构代码
public string DataTypeCode; //数据类型代码
public string AuxiliaryCode; //辅助控制码
public string PrintableGraphics; //可打印字符
public string TruncatedEscapeSequence; //截取转义字符
public byte LexicalLevel
{
get
{
if (TruncatedEscapeSequence == "%/A") return 2;
if (TruncatedEscapeSequence == "-A ") return 1;
return 0;
}
}
}
// 字段属性及格式
public class S57FieldAttrFormat
{
public string AttrName; //属性名
public string Format; //基础格式, b1,b2,A,R等
public int FormatLength; //格式的长度
public bool IsRepeated; //是否为重复属性
}
下面着手解析数据描述字段,补充相关代码:
// S57DataDescriptiveFields构造函数
public S57DataDescriptiveFields(S57Directory dir)
{
Fields = new List<S57DataDescriptiveField>();
foreach (var item in dir.Items)
{
if (item.Tag == "0000") continue;
Fields.Add(new S57DataDescriptiveField(item));
}
}
// S57DataDescriptiveField构造函数
public S57DataDescriptiveField(S57DirectoryItem item)
{
Tag = item.Tag;
FieldAttrFormats = new List<S57FieldAttrFormat>();
//解析字段控制说明
FieldControl = new S57FieldControl();
//解析字段名,直到遇到UT时结束
var index = BytesHelper.Position;
var chr = BytesHelper.GetChar();
while(chr != Helper.UT)
{
FieldName += chr;
chr = BytesHelper.GetChar();
}
//解析属性列表,直到遇到UT时结束
var isRepeated = false; //默认重复属性为False
var attrName = "";
chr = BytesHelper.GetChar();
while (chr != Helper.UT)
{
if(chr == '*') //重复属性
{
isRepeated = true;
chr = BytesHelper.GetChar();
continue;
}
if(chr == '!') //属性之间以!号分隔
{
FieldAttrFormats.Add(new S57FieldAttrFormat
{
AttrName = attrName,
IsRepeated = isRepeated
});
attrName = "";
chr = BytesHelper.GetChar();
continue;
}
attrName += chr;
chr = BytesHelper.GetChar();
}
if (attrName != "") //最后一个属性
{
FieldAttrFormats.Add(new S57FieldAttrFormat
{
AttrName = attrName,
IsRepeated = isRepeated
});
}
//解析属性对应的格式,直到遇到FT时结束。并将结果拆分成基础格式与长度
//基础格式包含:A, I, R, B, b1, b2, @
//格式以包含在括号中,并以,号为分隔,相邻且相同的格式可缩写,如下:
//(b11,b14,2b11,3A,2A(8),R(4),b11,2A,b11,b12,A)
chr = BytesHelper.GetChar();
var parenthesesNo = 0;
var formatIndex = -1; //当前的格式序号
var repeatedNo = 0; //格式的重复度
var formatLength = 0; //格式的长度
var format = ""; //基础格式
while (chr != Helper.FT)
{
if(chr == '(')
{
parenthesesNo += 1;
chr = BytesHelper.GetChar();
continue;
}
if (chr == ')')
{
parenthesesNo -= 1;
chr = BytesHelper.GetChar();
if(parenthesesNo > 0) continue;
}
if(chr == ',' ||
parenthesesNo == 0) //最后一个格式
{
if(FieldAttrFormats.Count == 0) //存在0001标签属性列表为空的情况
{
FieldAttrFormats.Add(new S57FieldAttrFormat
{
AttrName = "RCID"
});
}
if (repeatedNo == 0) repeatedNo = 1; //至少重复一次
if (format == "B") formatLength = formatLength / 8;
if (formatLength == 0) formatLength = -1; //长度为-1,表明长度不定
for (int i = 0; i < repeatedNo; i++)
{
formatIndex++;
FieldAttrFormats[formatIndex].Format = format;
FieldAttrFormats[formatIndex].FormatLength = formatLength;
}
//重置已存储的数据
repeatedNo = 0;
formatLength = 0;
format = "";
if(chr != Helper.FT) chr = BytesHelper.GetChar();
continue;
}
if(chr >= 48 && chr <= 57) //数字,要么是长度,要么是重复度
{
if(format == "") //重复度
{
repeatedNo = repeatedNo * 10 + chr - 48;
}
else //格式长度
{
formatLength = repeatedNo * 10 + chr - 48;
}
}
else //为字符
{
if(chr == 'b')
{
format = chr.ToString() + BytesHelper.GetChar();
}
else
{
format = chr.ToString();
}
}
chr = BytesHelper.GetChar();
}
}
// S57FieldControl构造函数
public S57FieldControl()
{
DataStructureCode = BytesHelper.GetChar();
DataTypeCode = BytesHelper.GetChar();
AuxiliaryCode = BytesHelper.GetString(2);
PrintableGraphics = BytesHelper.GetString(2);
TruncatedEscapeSequence = BytesHelper.GetString(3);
}
添加验证代码如下:
var ddfs = new S57DataDescriptiveFields(dir);
for (int i = 0; i < ddfs.Fields.Count; i++)
{
var f = ddfs.Fields[i];
Console.WriteLine($"{i.ToString().PadLeft(2)} {f.Tag} {f.FieldControl.DataStructureCode}{f.FieldControl.DataTypeCode}{f.FieldControl.AuxiliaryCode}{f.FieldControl.PrintableGraphics}{f.FieldControl.TruncatedEscapeSequence} {f.FieldName}");
foreach (var af in f.FieldAttrFormats)
{
Console.WriteLine($" |- {af.AttrName} {af.Format.PadLeft(2)} {af.FormatLength.ToString().PadLeft(3)} {af.IsRepeated}");
}
}
显示结果如下图,由图可知,标签DSID拥有16个属性,其中属性RCNM格式为b1(无符号整型),长度为1个字节,为非重复字段。
数据记录[DR]
数据记录[DR]的字段区必须以DDR中定义的前序遍历顺序编码,其结构由DDR中的数据描述字段定义。DR的头标区和目录区的解析过程与DDR一样。
顺序解析文件中的第一个DR:
//DR 头标区
Console.WriteLine();
Console.WriteLine("DR 头标区");
var drl = new S57Leader();
var drheader = drl.RecordLength.ToString() + drl.FieldControlString
+ drl.FieldAreaBaseAddress.ToString() + drl.ExCharacterSetIndicator
+ drl.FieldLengthSize.ToString() + drl.FieldPositionSize.ToString()
+ drl.Reserved.ToString() + drl.FieldTagSize.ToString();
Console.WriteLine(header);
//DR 目录区
Console.WriteLine();
Console.WriteLine("DR 目录区");
var drdir = new S57Directory(drl);
foreach (var di in drdir.Items)
{
Console.WriteLine($"{di.Tag}\t{di.FieldLength}\t{di.FieldPosition}");
}
上图显示,第一个DR的目录包含了0001、DSID和DSSI。可由DDR中的数据描述可知,0001包含1个属性,DSID包含16个属性,而DSSI包含11个属性。因此该DR字段区S57FieldArea
分为三段,对应类S57Field
。
- 对应有固定长度的格式,直接读取二进制文件按其格式解析;
- 而不定长度的格式,其以单元终止符UT分隔;
- 对于重复字段,会循环出现标签中的属性值,直到遇到字段终止符FT;
- 各子字段间以字段终止符FT分隔;
- 不同词汇级别,终止终长度不一样。
属性 | 格式 | 长度 | 二进制数据 | 解析后结果 |
---|---|---|---|---|
RCNM | b1 | 1 | 0A | 10 |
RCID | b1 | 4 | 01 0000 00 | 1 |
EXPP | b1 | 1 | 01 | 1 |
INTU | b1 | 1 | 04 | 4 |
DSNM | A | -1 | 55 5334 414B 3749 4D2E 3030 301F | US4AK7IM.000 |
EDTN | A | -1 | 311F | 1 |
UPDN | A | -1 | 301F | 0 |
UADT | A | 8 | 3230 3230 3034 3037 | 20200407 |
ISDT | A | 8 | 3230 3230 3034 3037 | 20200407 |
STED | R | 4 | 3033 2E31 | 3.1 |
PRSP | b1 | 1 | 01 | 1 |
PSDN | A | -1 | 1F | |
PRED | A | -1 | 322E 301F | 2.0 |
PROF | b1 | 1 | 01 | 1 |
AGEN | b1 | 2 | 26 02 | 500 |
COMT | A | -1 | 50 726F 6475 6365 6420 6279 204E 4F41 411F |
Produced by NOAA |
二进制可变长度格式中,1F代表UT,1E代表FT。
分析完其具体格式后,着手解析工作:
public class S57FieldArea
{
public List<S57Field> Fields;
public S57FieldArea(S57Directory dir, S57DataDescriptiveFields ddfs)
{
Fields = new List<S57Field>();
foreach (var item in dir.Items)
{
var ddf = ddfs.Fields.First(x => x.Tag == item.Tag);
Fields.Add(new S57Field(ddf, item.FieldLength));
}
}
}
public class S57Field
{
//标签名
public string Tag;
//属性
public string[] Attrs;
//属性对应的值
public dynamic[] Values;
public S57Field(S57DataDescriptiveField ddf, int fieldLength)
{
Tag = ddf.Tag;
var attrsLen = ddf.FieldAttrFormats.Count;
Attrs = new string[attrsLen];
Values = new dynamic[attrsLen];
//词汇级别2 意味着终止符长度为2,否则为1
var teminatorLen = ddf.FieldControl.LexicalLevel == 2 ? 2 : 1;
// 是否存在重复属性
var isRepeated = ddf.FieldAttrFormats[0].IsRepeated;
var repeatedNo = 1; //确定重复的次数
if (isRepeated)
{
var oldPos = BytesHelper.Position;
var lens = 0;
repeatedNo = 0;
while (lens < fieldLength-teminatorLen)
{
repeatedNo++;
for (int i = 0; i < attrsLen; i++)
{
var af = ddf.FieldAttrFormats[i];
var flen = af.FormatLength;
if (af.FormatLength == -1) //格式的长度不定,则终止符为单元终止符
{
flen = 0;
//根据终止符计算字段的长度
if (teminatorLen == 2)
{
while (!(BytesHelper.ENCBytes[oldPos + flen] == Helper.UT &&
BytesHelper.ENCBytes[oldPos + flen + 1] == 0))
{
flen++;
}
}
else
{
while (BytesHelper.ENCBytes[oldPos + flen] != Helper.UT)
{
flen++;
}
}
flen += teminatorLen; //加上终止符长度
}
oldPos += flen;
lens += flen;
}
}
}
for (int i = 0; i < attrsLen; i++)
{
Attrs[i] = ddf.FieldAttrFormats[i].AttrName;
if (isRepeated)
{
var type = ddf.FieldAttrFormats[i].Format;
var length = ddf.FieldAttrFormats[i].FormatLength;
switch (type)
{
case "b1":
switch (length)
{
case 1: Values[i] = new Byte[repeatedNo]; break;
case 2: Values[i] = new UInt16[repeatedNo]; break;
case 4: Values[i] = new UInt32[repeatedNo]; break;
}
break;
case "b2":
switch (length)
{
case 1: Values[i] = new SByte[repeatedNo]; break;
case 2: Values[i] = new Int16[repeatedNo]; break;
case 4: Values[i] = new Int32[repeatedNo]; break;
}
break;
case "B": Values[i] = new ulong[repeatedNo]; break;
case "I": Values[i] = new int[repeatedNo]; break;
case "R": Values[i] = new double[repeatedNo]; break;
case "@":
case "A": Values[i] = new string[repeatedNo]; break;
}
}
}
for (int j = 0; j < repeatedNo; j++)
{
for (int i = 0; i < attrsLen; i++)
{
var af = ddf.FieldAttrFormats[i];
var flen = af.FormatLength;
if (af.FormatLength == -1) //格式的长度不定,则终止符为单元终止符
{
flen = 0;
//根据终止符计算字段的长度
if (teminatorLen == 2)
{
while (!(BytesHelper.ENCBytes[BytesHelper.Position + flen] == Helper.UT &&
BytesHelper.ENCBytes[BytesHelper.Position + flen + 1] == 0))
{
flen++;
}
}
else
{
while (BytesHelper.ENCBytes[BytesHelper.Position + flen] != Helper.UT)
{
flen++;
}
}
}
if (isRepeated)
{
Values[i][j] = getSubFieldValue(af.Format, flen, ddf.FieldControl.LexicalLevel);
}
else
{
Values[i] = getSubFieldValue(af.Format, flen, ddf.FieldControl.LexicalLevel);
}
//跳过单元终止符
if (af.FormatLength == -1) BytesHelper.Position += teminatorLen;
}
}
//最后跳过字段终止符
BytesHelper.Position += teminatorLen;
}
private static dynamic getSubFieldValue(string type, int length, int lexicalLevel)
{
switch (type)
{
case "b1":
switch (length)
{
case 1:
return BytesHelper.GetByte();
case 2:
return BytesHelper.GetUInt16();
case 4:
return BytesHelper.GetUInt32();
default:
throw new Exception($"getSubFieldValue 解析出错[{type}][{length}]");
}
case "b2":
switch (length)
{
case 1:
return BytesHelper.GetSByte();
case 2:
return BytesHelper.GetInt16();
case 4:
return BytesHelper.GetInt32();
default:
throw new Exception($"getSubFieldValue 解析出错[{type}][{length}]");
}
case "A":
return BytesHelper.GetString(length, lexicalLevel);
case "B":
return BytesHelper.GetBitStr(length);
case "I":
return BytesHelper.GetInteger(length);
case "R":
return BytesHelper.GetDouble(length);
case "@":
return BytesHelper.GetString(length);
default:
throw new Exception($"getSubFieldValue 解析出错[{type}][{length}]");
}
}
}
为提高数据存储、读取效率,涉及到重复字段信息时都采用数组形式,而非列表形式存储。数组形式优点是效率高,缺点是事先需要知道数组长度,动态新增或删除元素时处理较为复杂(需添加额外代码)。此时,采用数组形式是为了程序处理效率,而牺牲了代码简洁度。
利用循环重复解析DR,添加验证代码如下:
//DR 字段区
Console.WriteLine();
Console.WriteLine("DR 字段区");
var fa = new S57FieldArea(drdir, ddfs);
foreach (var f in fa.Fields)
{
Console.WriteLine($"{f.Tag} ");
for (int i = 0; i < f.Attrs.Length; i++)
{
Console.Write($" |- {f.Attrs[i]} ");
if(f.Values[i] is IList)
{
Console.WriteLine();
foreach (var val in f.Values[i])
{
Console.WriteLine($" {val}");
}
}
else
{
Console.WriteLine($"{f.Values[i]}");
}
}
}
即可得解析结果如下: