Skip to content

官方文档(中文)

** 矢量瓦片**类似栅格瓦片,是将矢量数据用分割成矢量要素描述文件存储在服务器端,再到客户端根据指定样式进行渲染绘制地图。 mvt是mapbox 基于Google protocol buffers制定的矢量瓦片数据标准,被许多公司和组织采用。

1. 目标

本文档规定了一种节省存储空间的矢量瓦片数据编码格式。这种格式应用于客户端或服务端高效渲染或查询要素信息。

2. 文件格式

矢量瓦片文件采用Google Protocol Buffers进行编码。Google Protocol Buffers是一种兼容多语言、多平台、易扩展的数据序列化格式。

2.1. 文件后缀

矢量瓦片文件的后缀应该为mvt。例如,vector.mvt。

2.2 MIME类型

矢量瓦片的MIME类型应该设置为application/vnd.mapbox-vector-tile。

3. 投影和范围

矢量瓦片表示的是投影在正方形区块上的数据。矢量瓦片不应该包含范围和投影信息。解码方被假定知道矢量瓦片的范围和投影信息。 Web Mercator是默认的投影方式,Google tile scheme是默认的瓦片编号方式。两者一起完成了与任意范围、任意精度的地理区域的一一对应,例如https://example.com/17/65535/43602.mvt。 矢量瓦片可以用来表示任意投影方式、任意瓦片编号方案的数据。

4. 内部结构

这部分内容描述矢量瓦片的数据结构。读者需要先了解矢量瓦片protobuf编码方案文件中的结构定义。

4.1. 图层

矢量瓦片由一组命名的图层构成。每个图层包含几何要素元数据信息。设计的图层格式能够保证图层数据能够在内存中按顺序排列,由此在图层组末尾添加一个新的图层就不用更改已有的数据。

每块矢量瓦片应该至少包含一个图层。每个图层应该至少包含一个要素。

图层必须包含一个version字段表示此图层所遵守的《矢量瓦片标准》的主版本号。例如,某个图层遵守2.1版本的标准,那么它的version字段的值则为整数2。version字段应该设定为图层的第一个字段。解码器应该首先解析version字段,以确定是否能够解析该版本的图层。当遇到一个未知版本的矢量瓦片图层时,解码器可以尝试去解析它,或者可以跳过该图层。以上两种情况下,解码器都应该继续解析后续的图层。

图层必须包含一个name字段。每块矢量瓦片必须不包含两个或两个以上的图层具有相同name值。在向一块矢量瓦片添加一个新的图层之前,编码器必须检查已有的name值以防止重复。

图层中的每个要素可以包含一个或多个key-value作为它的元数据(见下文)。所有要素的key和value被分别索引为两个列表——keysvalues——为图层中的所有要素所共享。

图层keys字段的每个元素都是字符串。keys字段包含了图层中所有要素的key,并且每个key可以通过它在keys列表中的索引号引用,第一个key的索引号是0 。keys列表必须不包含两个或两个以上key是一样的。 图层values字段的每个元素是多种类型的值的编码(见下文)。values字段包含了图层中所有要素的value,并且每个value可以通过它在values列表中的索引号引用,第一个value的索引号是0 。values列表必须不包含两个或两个以上value是一样的。 为了支持字符串型、布尔型、整型、浮点型多种类型的值,对value字段的编码包含了一组optional字段。每个value必须包含其中的一个字段。

图层必须包含一个extent字段,表示瓦片的宽度和高度,以整数表示。矢量瓦片中的几何坐标可以超出extent定义的范围。超出extent范围的几何要素被经常用来作为缓冲区,以渲染重叠在多块相邻瓦片上的要素。

例如,如果一块瓦片的extent范围是4096,那么坐标的单位是瓦片长宽的1/4096。坐标0在瓦片的顶部或左边缘,坐标4096在瓦片的底部或右边缘。坐标从1到4095都是在瓦片内部,坐标小于0或者大于4096在瓦片外部。坐标(1,10)或(4095,10)在瓦片内部。坐标(0,10)或(4096,10)在瓦片边缘。坐标(-1,10)或(4097,10)在瓦片外部。

4.2. 要素

每个要素必须包含一个geometry字段。 每个要素必须包含一个type字段,该字段将在几何类型章节描述(4.3.4)。 每个要素可以包含一个tags字段。如果存在属于要素级别的元数据,应该存储到tags字段中。 每个要素可以包含一个id字段。如果一个要素包含一个id字段,那么id字段的值应该相对于图层中的其他要素是唯一的。

4.3. 几何图形编码

矢量瓦片中的几何数据被定义为屏幕坐标系。瓦片的左上角(显示默认如此)是坐标系的原点。X轴向右为正,Y轴向下为正。几何图形中的坐标必须为整数。 几何图形被编码为要素的geometry字段的一个32位无符号型整数序列。每个整数是CommandInteger或者ParameterInteger。解码器解析这些整数序列作为生成几何图形的一系列有序操作。 指令涉及到的位置是相对于“游标”的,即一个可重定义的点。对于要素中的第一条指令,游标在坐标系中的位置是(0,0)。有些指定能够移动游标,因而会影响到接下来执行的指令。

4.3.1. 指令数

CommandInteger指代所要执行的操作和执行的次数,分别以command ID和command count表示。 command IDCommandInteger最末尾的3个比特位表示,即从0到7。command countCommandInteger剩下的29个比特位表示,即0到pow(2, 29) - 1。 command IDcommand count、和CommandInteger三者可以通过以下位运算相互转换。 CommandInteger = (id & 0x7) | (count << 3)id = CommandInteger & 0x7count = CommandInteger >> 3 每个command ID表示以下指令中的一种:

指令Id参数参数个数
MoveTo1dX, dY2
LineTo2dX, dY2
ClosePath7无参数0
指令数示例
指令IDCountCommandInteger二进制表示[Count][Id]
MoveTo119[00000000 00000000 0000000 00001][001]
MoveTo1120961[00000000 00000000 0000011 11000][001]
LineTo2110[00000000 00000000 0000000 00001][010]
LineTo2326[00000000 00000000 0000000 00011][010]
ClosePath7115[00000000 00000000 0000000 00001][111]

4.3.2. 参数数

指令的所有参数紧跟在ParameterInteger之后。跟在CommandInteger之后的ParameterIntegers个数等于指令所需要参数的个数乘以指令执行的次数。例如,一条指示MoveTo指令执行3次的CommandInteger之后会跟随6个ParameterIntegersParameterIntegerzigzag方式编码得到,以使小负数和正数都被编码为小整数。将参数值编码为ParameterInteger按以下公式转换: ParameterInteger = (value << 1) ^ (value >> 31) 参数值不支持大于pow(2,31) - 1或-1 * (pow(2,31) - 1)的数值。 以下的公式用来将ParameterInteger解码为实际值: value = ((ParameterInteger >> 1) ^ (-(ParameterInteger & 1)))

4.3.3. 指令类型

以下关于指令的描述中,游标的初始位置定义为坐标(cX, cY),其中cX指代游标在X轴上的位置,cY指代游标在Y轴上的位置。

4.3.3.1. MoveTo指令

表示MoveTo指令执行n的ParameterInteger必须立即接上n对ParameterInteger。对于(dX, dY)参数:

  1. 定义坐标(pX, pY),其中pX = cX + dX和pY = cY + dY。
    • 对于点要素,这个坐标定义了一个新的点要素。
    • 对于线要素,这个坐标定义了一条新的线要素的起点。
    • 对于面要素,这个坐标定义了一个新环的起点。
  2. 将游标移至(pX, pY)。
4.3.3.1. LineTo指令

表示LineTo指令执行n的ParameterInteger必须立即接上n对ParameterInteger。对于(dX, dY)参数:

  1. 定义一条以游标位置(cX, cY)为起点,(pX, pY)为终点的线段,其中pX = cX + dX和pY = cY + dY。
    • 对于线要素,这条线段延长了当前线要素。
    • 对于面要素,这条线段延长了当前环。
  2. 将游标移至(pX, pY)。

对于任意一对(dX, dY),dX和dY必须不能同时为0.

4.3.3.3. ClosePath指令

每条ClosePath指令必须只能执行一次并且无附带参数。这条指令通过构造一条以游标(cX, cY)为起点、当前环的起点为终点的线段,闭合面要素的当前环。 这条指定不改变游标的位置。

4.3.4. 几何类型

要素geometry字段的type的取值必须是GeomType枚举值之一。支持的几何类型如下:

  • UNKNOWN
  • POINT
  • LINESTRING
  • POLYGON

不支持GeometryCollection类型。

4.3.4.1. Unknown几何类型

本标准有意设置一个Unknown几何类型。这种几何类型可以用来编码试验性的几何类型。解码器可以选择忽略这种几何类型的要素。

4.3.4.2. Point几何类型

POINT几何类型用来表示单点或多点几何。每个点几何的指令序列必须包含一个MoveTo指令,并且该指令的command count大于0。 如果POINT几何的MoveTo的command count为1,那么必须将其解析为单点;否则必须解析为多点,指令后面的每对ParameterInteger表示一个单点。

4.3.4.3. Linestring几何类型

LINESTRING几何类型用来表示单线或多线几何。线几何的指令序列必须包含一个或多个下列序列:

  1. 一个MoveTo指令,其command count为1
  2. 一个LineTo指令,其command count大于0

如果LINESTRING的指令序列只包含1个MoveTo指令,那么必须将其解析为单线;否则,必须将其解析为多线,其中的每个MoveTo指令开始构造一条新线几何。

4.3.4.4. Polygon几何类型

POLYGON几何类型表示面或多面几何,每个面有且只有一个外环和零个或多个内环。面几何的指令序列包含一个或多个下列序列:

  1. 一个ExteriorRing
  2. 零个或多个InteriorRing

每个ExteriorRingInteriorRing必须包含以下序列:

  1. 一个MoveTo指令,其command count为1
  2. 一个LineTo指令,其command count大于1
  3. 一个ClosePath指令

一个外环被定义为一个线性的环,当应用surveyor's formula,以多边形的节点在瓦片坐标系下的坐标计算面积时,其面积为正。在瓦片坐标系下(X向右为正,Y向下为正),外环节点以顺时针旋转。

一个内环被定义为一个线性的环,当应用surveyor's formula,以多边形的节点在瓦片坐标系下的坐标计算面积时,其面积为负。在瓦片坐标系下(X向右为正,Y向下为正),内环节点以逆时针旋转。

如果POLYGON的指令序列只包含一个外环,那么必须将其解析为单面;否则,必须解析为多面几何,其中每个外环表示一个新面的开始。如果面几何包换内环,那么必须将其编码到所属的外环之后。

线性环必须不包含异常点,例如自相交或自相切。在ClosePath之前的坐标不应该与线性环的起始点坐标相同,因为会产生零长度的线段。线性环经过surveyor's formula计算的面积不应该为0,因为这意味着环包含有异常点。 面几何必须不能有内环相交,并且内环必须被包围在内环之中。

4.3.5. 几何要素编码示例

4.3.5.1. 点要素示例

假设示例点的坐标为:

  • (25, 17)

表示它只需要一条指令:

  • MoveTo(+25, +17)
javascript
编码      : [ 9 50 34 ]
              | |  `> 解码: ((34 >> 1) ^ (-(34 & 1))) = +17
              | `> 解码: ((50 >> 1) ^ (-(50 & 1))) = +25
              | ===== 相对地 MoveTo(+25, +17) == 创建点 (25,17)
              `> [00001 001] = command id 1 (MoveTo), command count 1
4.3.5.2. 多点要素示例

假设多点要素的坐标为:

  • (5,7)
  • (3,2)

编码需要两条指令:

  • MoveTo(+5,+7)
  • MoveTo(-2,-5)
javascript
编码      : [ 17 10 14 3 9 ]
               |  |  | | `> 解码: ((9 >> 1) ^ (-(9 & 1))) = -5
               |  |  | `> 解码: ((3 >> 1) ^ (-(3 & 1))) = -2
               |  |  | === 相对地 MoveTo(-2, -5) == 创建点 (3,2)
               |  |  `> 解码: ((34 >> 1) ^ (-(34 & 1))) = +7
               |  `> 解码: ((50 >> 1) ^ (-(50 & 1))) = +5
               | ===== 相对地 MoveTo(+5, +7) == 创建点 (5,7)
               `> [00010 001] = command id 1 (MoveTo), command count 2
4.3.5.3. 线要素示例

假设示例线要素的坐标为:

  • (2,2)
  • (2,10)
  • (10,10)

编码需要3条指令:

  • MoveTo(+2,+2)
  • LineTo(+0,+8)
  • LineTo(+8,+0)
javascript
编码      : [ 9 4 4 18 0 16 16 0 ]
              |      |      ==== 相对地 LineTo(+8, +0) == 连接到点 (10, 10)
              |      | ==== 相对地 LineTo(+0, +8) == 连接到点 (2, 10)
              |      `> [00010 010] = command id 2 (LineTo), command count 2
              | === 相对地 MoveTo(+2, +2)
              `> [00001 001] = command id 1 (MoveTo), command count 1
4.3.5.4. Example Multi Linestring
4.3.5.4. 多线要素示例

假设示例要素的坐标为:

  • Line 1:
    • (2,2)
    • (2,10)
    • (10,10)
  • Line 2:
    • (1,1)
    • (3,5)

编码需要以下指令:

  • MoveTo(+2,+2)
  • LineTo(+0,+8)
  • LineTo(+8,+0)
  • MoveTo(-9,-9)
  • LineTo(+2,+4)
javascript
编码      : [ 9 4 4 18 0 16 16 0 9 17 17 10 4 8 ]
              |      |           |        | === 相对地 LineTo(+2, +4) == 连接到点 (3,5)
              |      |           |        `> [00001 010] = command id 2 (LineTo), command count 1
              |      |           | ===== 相对地 MoveTo(-9, -9) == 新建一条线从 (1,1)
              |      |           `> [00001 001] = command id 1 (MoveTo), command count 1
              |      |      ==== 相对地 LineTo(+8, +0) == 连接到点 (10, 10)
              |      | ==== 相对地 LineTo(+0, +8) == 连接到点 (2, 10)
              |      `> [00010 010] = command id 2 (LineTo), command count 2
              | === 相对地 MoveTo(+2, +2)
              `> [00001 001] = command id 1 (MoveTo), command count 1
4.3.5.5. 面要素示例

假设示例面要素的坐标为:

  • (3,6)
  • (8,12)
  • (20,34)
  • (3,6) 闭合

编码需要以下指令:

  • MoveTo(3, 6)
  • LineTo(5, 6)
  • LineTo(12, 22)
  • ClosePath
javascript
编码      : [ 9 6 12 18 10 12 24 44 15 ]
              |       |              `> [00001 111] command id 7 (ClosePath), command count 1
              |       |       ===== 相对地 LineTo(+12, +22) == 连接到点 (20, 34)
              |       | ===== 相对地 LineTo(+5, +6) == 连接到点 (8, 12)
              |       `> [00010 010] = command id 2 (LineTo), command count 2
              | ==== 相对地 MoveTo(+3, +6)
              `> [00001 001] = command id 1 (MoveTo), command count 1
4.3.5.6. 多面要素示例

示例要素包含两个多边形,其中一个多边形有一个洞。多边形中的点如下。注意,多边形中的点环绕顺序非常重要,应为这个顺序被用来区别外环和内环。

  • Polygon 1:
    • 外环:
      • (0,0)
      • (10,0)
      • (10,10)
      • (0,10)
      • (0,0) 闭合
  • Polygon 2:
    • 外环:
      • (11,11)
      • (20,11)
      • (20,20)
      • (11,20)
      • (11,11) 闭合
    • 内环:
      • (13,13)
      • (13,17)
      • (17,17)
      • (17,13)
      • (13,13) 闭合

编码需要以下一系列指令:

  • MoveTo(+0,+0)
  • LineTo(+10,+0)
  • LineTo(+0,+10)
  • LineTo(-10,+0) // 执行这条指令后,游标的位置在(0, 10)
  • ClosePath // Polygon 1结束
  • MoveTo(+11,+1) // 这条指令相对于上面最后一条LineTo指令!
  • LineTo(+9,+0)
  • LineTo(+0,+9)
  • LineTo(-9,+0) // 执行这条指令后,游标的位置在(11, 20)
  • ClosePath // 这是一个新面要素,因为面积为正
  • MoveTo(+2,-7) // 这条指令相对于上面最后一条LineTo指令!
  • LineTo(+0,+4)
  • LineTo(+4,+0)
  • LineTo(+0,-4) // 执行这条指令后,游标的位置在(17, 13)
  • ClosePath // 这是一个内环,因为面积为负

4.4. 要素属性

要素属性被编码为tag字段中的一对对整数。在每对tag中,第一个整数表示key在其所属的layer的keys列表的中索引号(以0开始)。第二个整数表示value在其所属的layer的values列表的中索引号(以0开始)。一个要素的所有key索引必须唯一,以保证要素中没有重复的属性项。每个要素的tag字段必须为偶数。要素中的tag字段包含的key索引号或value索引号必须不能大于或等于相应图层中keys或values列表中的元素数目。

4.5示例

例如,一个GeoJSON格式的要素如下:

json
{
    "type": "FeatureCollection",
    "features": [
        {
            "geometry": {
                "type": "Point",
                "coordinates": [
                    -8247861.1000836585,
                    4970241.327215323
                ]
            },
            "type": "Feature",
            "properties": {
                "hello": "world",
                "h": "world",
                "count": 1.23
            }
        },
        {
            "geometry": {
                "type": "Point",
                "coordinates": [
                    -8247861.1000836585,
                    4970241.327215323
                ]
            },
            "type": "Feature",
            "properties": {
                "hello": "again",
                "count": 2
            }
        }
    ]
}

会被结构化为:

javascript
layers {
  version: 2
  name: "points"
  features: {
    id: 1
    tags: 0
    tags: 0
    tags: 1
    tags: 0
    tags: 2
    tags: 1
    type: Point
    geometry: 9
    geometry: 2410
    geometry: 3080
  }
  keys: "hello"
  keys: "h"
  keys: "count"
  values: {
    string_value: "world"
  }
  values: {
    double_value: 1.23
  }
  values: {
    string_value: "again"
  }
  values: {
    int_value: 2
  }
  extent: 4096
}

mvt 文件在线解析

为了更加深入理解mvt,可以使用这个开源库解析矢量瓦片数据。 以下是笔者使用该库解析的一份数据: image.png

总结

MVT格式为了减少矢量数据存储的大小,采用以下方式:

  1. 充分采用了索引技术,一份矢量数据中相同的数据值(坐标、属性)只存储一份,然后通过索引去引用。
  2. 用指令去存储几何信息
  3. 不存储范围和投影信息,而假设接收方知道坐标的投影信息。

几个概念的区分:

  • Google Protocol Buffers,是Google用于序列化结构化数据的语言中立、平台中立、可扩展的机制。
  • Mapbox Vector 是mapbox公司一种矢量瓦片规范,采用Google Protocol Buffers进行编码。
  • mapboxgl 渲染矢量数据的流程是:首先对MVT格式进行解析为一种更方便读取的格式后,在进行渲染。(https://github.com/mapbox/vector-tile-js,没有看源码,应该是解析为这个库的格式吧?)