以下是《Mapbox Vector Tile Specification》最新的2.1版的中文翻译,以便查阅。

矢量瓦片标准

本文档中的“必须”、“必须不”、“必备”、”应该“、“不应该”、“建议”、“可以”、“可选”的含义参照RFC 2119

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字段的值则为整数2version字段应该设定为图层的第一个字段。解码器应该首先解析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 ID以CommandInteger最末尾的3个比特位表示,即从0到7。command count以CommandInteger剩下的29个比特位表示,即0pow(2, 29) - 1

command ID、command count、和CommandInteger三者可以通过以下位运算相互转换。

1
CommandInteger = (id & 0x7) | (count << 3)
1
id = CommandInteger & 0x7
1
count = CommandInteger >> 3

每个command ID表示以下指令中的一种:

指令 Id 参数 参数个数
MoveTo 1 dX, dY 2
LineTo 2 dX, dY 2
ClosePath 7 无参数 0
指令数示例
指令 ID Count CommandInteger 二进制表示[Count][Id]
MoveTo 1 1 9 [00000000 00000000 0000000 00001][001]
MoveTo 1 120 961 [00000000 00000000 0000011 11000][001]
LineTo 2 1 10 [00000000 00000000 0000000 00001][010]
LineTo 2 3 26 [00000000 00000000 0000000 00011][010]
ClosePath 7 1 15 [00000000 00000000 0000000 00001][111]

4.3.2. 参数数

指令的所有参数紧跟在ParameterInteger之后。跟在CommandInteger之后的ParameterIntegers个数等于指令所需要参数的个数乘以指令执行的次数。例如,一条指示MoveTo指令执行3次的CommandInteger之后会跟随6个ParameterIntegers

ParameterIntegerzigzag方式编码得到,以使小负数和正数都被编码为小整数。将参数值编码为ParameterInteger按以下公式转换:

1
ParameterInteger = (value << 1) ^ (value >> 31)

参数值不支持大于pow(2,31) - 1-1 * (pow(2,31) - 1)的数值。

以下的公式用来将ParameterInteger解码为实际值:

1
value = ((ParameterInteger >> 1) ^ (-(ParameterInteger & 1)))

4.3.3. 指令类型

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

4.3.3.1. MoveTo指令

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

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

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

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

对于任意一对(dX, dY)dXdY必须不能同时为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

Each ExteriorRing and InteriorRing MUST consist of the following sequence:
每个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)
1
2
3
4
5
编码      : [ 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)
1
2
3
4
5
6
7
8
编码      : [ 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
| ===== relative MoveTo(+25, +17) == 创建点 (25,17)
`> [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)
1
2
3
4
5
6
编码      : [ 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)
1
2
3
4
5
6
7
8
9
10
编码      : [ 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
1
2
3
4
5
6
7
编码      : [ 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在其所属的layerkeys列表的中索引号(以0开始)。第二个整数表示value在其所属的layervalues列表的中索引号(以0开始)。一个要素的所有key索引必须唯一,以保证要素中没有重复的属性项。每个要素的tag字段必须为偶数。要素中的tag字段包含的key索引号或value索引号必须不能大于或等于相应图层中keysvalues列表中的元素数目。

4.5. 示例

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
{
"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
}
}
]
}

会被结构化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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
}

注意几何要素的实际坐标取决于坐标系和瓦片的范围。