2.2.9 电子海图系统解析及开发 海图显示 - 矢量符号描述语言

符号

不同用途的灯标、锚地边界、岸上建筑物等物标在海图都有不同的符号,

符号尺度及转心示意图

符号所在画布大小为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毫米。
  • PUx坐标, y坐标 [,x, y, ... x, y]
       指的是画笔移到坐标(x, y)处,此时并没有绘制。
  • PDx坐标, y坐标 [,x, y, ... x, y]
       指的是画笔从当前位置画到坐标(x, y)处,绘制完毕后,画笔位置会更新,画笔颜色及大小由之前指令确定。
  • CI半径
       绘制指定半径大小的圆,圆心为当前位置,绘制完毕后,画笔位置不会更新。
  • AAx坐标, y坐标, 角度
       绘制圆弧,参数中的坐标(x, y)为弧线圆心的位置,弧线的起点为画笔当前位置。当角度为正时,表示逆时针旋转,否则为顺时针旋转指定的角度数。绘制完毕后,画笔位置会更新到圆弧终点。
  • PMn
       多边形模式。用于存储将要绘制的多边形,直到多边形被完全定义完。例如,要定义多边形,需要将笔移到所需位置,然后执行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;",
        }
    );
矢量符号指令绘制结果
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,504评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,434评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,089评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,378评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,472评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,506评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,519评论 3 413
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,292评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,738评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,022评论 2 329
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,194评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,873评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,536评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,162评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,413评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,075评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,080评论 2 352

推荐阅读更多精彩内容