符号
不同用途的灯标、锚地边界、岸上建筑物等物标在海图都有不同的符号,
符号所在画布大小为32767个单位(每一单位代表0.01毫米)。
- 每个符号有一个全局唯一的名称,也拥有一套坐标系,坐标原点位于左上角。
- 符号绘制的起点为(BoundingBoxX, BoundingBoxY),绘制区拥有一定高度(BoundingBoxHeight)与宽度(BoundingBoxWidth)。
- 符号还存在一个转心(PivotPointX, PivotPointY),但转心不一定位于绘制区内部。在指定坐标处绘制符号时,应该将转心位于指定坐标处;如果需要旋转符号,则旋转的中心也为转心。
- 符号的具体形状由一系列指令(Instructions,由矢量符号描述语言编写)构成。
矢量符号描述语言
S-52标准中,是用矢量符号描述语言(Vector Symbol Description Language)来定义这些符号形状的。这些符号被广泛用于电子海图中定义点、复杂的线型及填充复杂的图案。
矢量符号描述语言使用一支虚构的“笔”,上绘画,然后记录画笔移动的位置,最终形式电子海图所需的矢量符号。与屏幕坐标一样,画布坐标的原点(位置0,0)在图形的左上角,x坐标向右延伸,y坐标向下延伸。矢量符号描述语言画笔的颜色、大小、移动等都是通过指令实现的,其中:
- ; 不同的指令用分号隔开
- , 如果指令含有多个参数,参数之间用逗号分隔;如果没有参数,则无需添加逗号
-
SP 颜色
表示画笔的颜色,该颜色用一个字母代表某种颜色标记。SP指令中画笔颜色对作用于其之后的指令,直到新的画笔出现为止。 -
ST 透明度
表示当前颜色存在一定比例的透明度。一共四级(0~3),1级代表25%的透明度。 -
SW 画笔宽度
规定画笔宽度为几个单位,每一个单位代表0.3毫米。 -
PU x坐标, y坐标 [,x, y, ... x, y]
指的是画笔移到坐标(x, y)处,此时并没有绘制。 -
PD x坐标, y坐标 [,x, y, ... x, y]
指的是画笔从当前位置画到坐标(x, y)处,绘制完毕后,画笔位置会更新,画笔颜色及大小由之前指令确定。 -
CI 半径
绘制指定半径大小的圆,圆心为当前位置,绘制完毕后,画笔位置不会更新。 -
AA x坐标, y坐标, 角度
绘制圆弧,参数中的坐标(x, y)为弧线圆心的位置,弧线的起点为画笔当前位置。当角度为正时,表示逆时针旋转,否则为顺时针旋转指定的角度数。绘制完毕后,画笔位置会更新到圆弧终点。 -
PM n
多边形模式。用于存储将要绘制的多边形,直到多边形被完全定义完。例如,要定义多边形,需要将笔移到所需位置,然后执行PM 0进入多边形模式,然后指定适当的指令以定义多边形的形状。如果还要定义子多边形,以PM 1指令结束形状并定义下一个形状,直到执行PM 2退出多边形模式。 -
EP
绘制之前存储的多边形的边界。 -
FP
填充之前存储的多边形。 -
SC 符号名, 方向
在指定的方向上绘制另一个符号,符号的转心(Pivot point)位于当前画笔位置。 如果方向=0,表示符号维持原方向;方向=1,表示方向为最后画笔绘制的方向;方向=2,表示符号沿符号化方向旋转90度。
示例
SPA;SW1;PU1000,1000;PD1000,2000;
画笔颜色'A',宽度为1个单位,移动到(1000, 1000),绘制垂直线段到(1000, 2000)。
SPB;SW2;PU1000,1000;PD1000,2000,2000,2000,2000,1000,1000,1000;
画笔颜色'B',宽度为2个单位,移动到(1000, 1000),绘制线段到(1000, 2000),接着一直绘制到(2000, 2000),(2000, 1000),(1000, 1000),最终形成一个矩形。
SPB;ST2;PM0;PU1000,1000;PD1000,2000,2000,2000,2000,1000;PM2;FP;
进入多边形模式(PM0),绘制上个示例中的矩形(多边形最后自动首尾相连),最后退出多边形模式(PM2),用画笔颜色'B',50%的透明度,填充多边形。
PU100,100;PM0;CI50;PM2;SPE;ST0;FP;SPA;EP;
以(100, 100)为圆心,150为半径构造一个圆。然后设置画笔颜色‘E',透明度为0%,填充该圆。重新设置画笔颜色'A',为圆描边。
SPU;SW1;PU100,100;PD200,100;AA200,150,-90;PD250,200;
从(100, 100)开始,画一条水平线到(200, 100);接着以(200, 100)为起点,以(200, 150)为圆心,顺序针画一个90度的圆弧,画笔移到圆弧的终点(将会是(250, 150));最后画一条直线到(250, 200)。
SPC;SW3;PU500,500,1000,1000;SCsample99,1;PD1000,500;
移动画笔从(500, 500)到(1000, 1000),方位为135度,沿该方向画一个符号(sample99),符号的转心位于(1000, 1000)处;接着从(1000, 1000),用颜色'C',宽度为3个单位的画笔画一条直线到(1000, 500)。
编码实现
新建一个静态的工具类S52Tools
,其中方法DrawSymbol
实现上述指令:
public static class S52Tools
{
//ca 画布
//colors 颜色字典,索引是SP指令后的字母
//instructions 矢量符号描述符号组成的指令
public static void DrawSymbol(SKCanvas ca, Dictionary<char, SKColor> colors, List<string> instructions)
{
bool isPMExist = false;
int width = 1; //默认宽度
var color = S52Colors.Instance["NODTA"]; //默认颜色
var pen = new SKPaint() { Color = color, StrokeWidth = width, Style = SKPaintStyle.Stroke }; //默认画笔
SKPath path = new SKPath();
foreach (var command in instructions)
{
SKPath path = new SKPath();
string[] subcomm = command.Split(';');
foreach (var item in subcomm)
{
if (item.Length >= 2)
{
int transparency = 0;
switch (item.Substring(0, 2))
{
case "SP": //获取颜色
color = colors[item[2]];
pen.Color = color;
break;
case "ST": //获取透明度
transparency = item[2] - 48;
color = color.WithAlpha((byte)(255 - transparency * 255 / 4));
pen.Color = color;
break;
case "SW": //获取画笔宽度
width = (item[2] - 48) * 30; // 30 = 0.3/0.01
pen.StrokeWidth = width;
break;
case "PU": //起点 PU500,500,1000,1000;
string[] pu = item.Substring(2).Split(',');
for (int i = 0; i < pu.Length-1; i+=2)
{
path.MoveTo(new SKPoint(int.Parse(pu[i]), int.Parse(pu[i+1])));
}
break;
case "PD": //终点 PD; PD500,500; PD500,500,1000,1000;
string[] pd = item.Substring(2).Split(',');
if (pd.Length == 1) //PD;
{
ca.DrawPoint(path.LastPoint, pen);
}
else
{
for (int i = 0; i < pd.Length - 1; i += 2)
{
SKPoint end = new SKPoint(int.Parse(pd[i]), int.Parse(pd[i + 1]));
if (path.LastPoint == end)
{
ca.DrawPoint(end, pen);
continue;
}
path.LineTo(end);
}
}
break;
case "CI":
int radius = int.Parse(item.Substring(2));
path.AddCircle(path.LastPoint.X, path.LastPoint.Y, radius);
break;
case "AA":
string[] xyd = item.Substring(2).Split(',');
var x = int.Parse(xyd[0]);
var y = int.Parse(xyd[1]);
var d = int.Parse(xyd[2]);
var r = (float)Math.Sqrt(GeoTools.DistanceSqrd(x, y, (int)path.LastPoint.X, (int)path.LastPoint.Y));
var rect = new SKRect(x - r, y - r, x + r, y + r);
//从正北起算,而绘制时以正东起算,所以需要减掉90度
var startAngle = GeoTools.AngleBetweenTwoPoints((int)path.LastPoint.X, (int)path.LastPoint.Y, x, y);
path.ArcTo(rect, (float)startAngle - 90, -1 * d, false);
break;
case "PM":
switch (item[2] - 48)
{
case 0: isPMExist = true; break;
case 1: path.Close(); break;
case 2: isPMExist = false; break;
}
break;
case "EP":
pen.Style = SKPaintStyle.Stroke;
ca.DrawPath(path, pen);
break;
case "FP":
pen.Style = SKPaintStyle.Fill;
ca.DrawPath(path, pen);
break;
case "SC": //ECDIS中并没有用到该指令,暂不用解析
throw new Exception("S52 [SC]绘制没有解析");
default:
throw new Exception($"S52 [{item.Substring(0, 2)}]绘制没有解析");
}
}
}
}
if (!isPMExist)
{
pen.Style = SKPaintStyle.Stroke;
ca.DrawPath(path, pen);
}
}
}
调用过程及绘制结果如下:
ca.Scale(0.1f);
S52Tools.DrawSymbol(
ca, //画布
new Dictionary<char, SKColor> { //颜色字典
{ 'A', SKColors.Red },
{ 'B', SKColors.Blue },
{ 'C', SKColors.Green },
},
new List<string>() { //指令
"SPB;ST2;PM0;PU1000,1000;PD1000,2000,2000,2000,2000,1000;PM2;FP;",
"SPA;SW2;PU1000,1000;PD1000,2000,2000,2000,2000,1000,1000,1000;",
"SPC;SW2;PU1000,1000;PD2000,1000;AA2000,1500,-90;PD2500,2000;",
}
);