前言
文中的代码表达采用Java,如果不想看原理,可以直接跳到博客末尾,有我写的一个Java版的demo 一般来说,轨迹是由若干个轨迹点组成的数组来表达的,每个轨迹点可以表达为 <math xmlns="http://www.w3.org/1998/Math/MathML"> p = ( x , y , t , A ) p = (x, y, t, A) </math>p=(x,y,t,A)。其中 <math xmlns="http://www.w3.org/1998/Math/MathML"> x , y x, y </math>x,y 是其空间坐标(一般来说,空间坐标用经纬度表示,即 <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x一般对应经度(longitude ), <math xmlns="http://www.w3.org/1998/Math/MathML"> y y </math>y一般对应纬度(latitude )), <math xmlns="http://www.w3.org/1998/Math/MathML"> t t </math>t是记录这个轨迹点的时间戳(Timestamp ), <math xmlns="http://www.w3.org/1998/Math/MathML"> A A </math>A则是这个轨迹点的属性。如果只想保留一个轨迹点最基本的信息,则只需要保留其空间信息和时间信息,即可以将一个轨迹点表示为 <math xmlns="http://www.w3.org/1998/Math/MathML"> p = ( x , y , t ) p = (x, y, t) </math>p=(x,y,t)。在写代码时,一个轨迹点的类型可以定义如下:
Java
class PointTs {
double lng;
double lat;
long timestamp;
}
而这样保存一个轨迹点, <math xmlns="http://www.w3.org/1998/Math/MathML"> x x </math>x和 <math xmlns="http://www.w3.org/1998/Math/MathML"> y y </math>y一般使用double 类型, <math xmlns="http://www.w3.org/1998/Math/MathML"> t t </math>t一般使用long 类型,再加上至少3个字符的分隔符(分隔x,y,t和每个轨迹点),一个轨迹点就要占 Double.SIZE * 2 + Long.SIZE + Character.SIZE * 3 == 27 * Byte.SIZE
, 也就是说保存一个轨迹点至少需要占27个字节。在如今轨迹数量越来越大的情况下,有必要使用轨迹压缩的算法来解决轨迹数据量过大的问题。在查阅了资料后,我在Google地图开发平台中看到了Google采用的轨迹数据压缩算法,名为Polyline encoding algorithm(需要科学上网),可以将若干个轨迹点编码成一串字符串,并且实现轨迹的压缩。这是一个有损的压缩算法,并且可以压缩有符号的数字。 接下来我将从编码和解码两个方面解析这个算法,并给出示例代码。
编码
Polyline encoding算法的核心思想是对一个浮点数进行压缩,比如说,有一个经度为: -179.9832104
对于这样的浮点型数字,首先需要做的是想办法去掉其小数点。所以,可以预设一个10的整数倍的值来与这个浮点数相乘,并取得整数。
-
在这里,我预设这个值为 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 0 5 10^5 </math>105,将这个经度乘以 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 0 5 10^5 </math>105,得到:
-17998321
-
将上一步得到的整数转换成二进制,由于我们的值是负数,得到的是
17998321
的补码:11111110 11101101 01011110 00001111
-
将二进制数左移一位:
11111101 11011010 10111100 00011110
-
如果原来的数是负数,则取反码:
00000010 00100101 01000011 11100001
3、4两步的目的是为了区分正负数,这样做后,负数的最后一位都是1
,正数的最后一位都是0
-
从右到左,每5位分为一组(一组称为一个chunk):
00001 00010 01010 10000 11111 00001
这样可以舍弃高位的0,节约空间。 -
将这些分组顺序反转(reverse order):
00001 11111 10000 01010 00010 00001
这样做的目的是在写代码时可以每次从低位取数据,更适合代码实现。因为一般而言,写代码读二进制时,读高位比较复杂,而读低位较简单(例如,读最后五位只需要&0x1F
),读到最后几位后可以直接append到结果中,不用插入到结果的前端,效率更高。 -
除了最后一个chunk,其他chunk和
0x20
进行或
操作:100001 111111 110000 101010 100010 000001
这一步的目的是保证只有最后一个chunk是小于0x20
的。起到了分隔的作用。 -
分别将每个二进制的chunk转化为十进制的数字:
33 63 48 42 34 1
-
对每个数字都加上63:
96 126 111 105 97 64
由于ASCII码前63位中有很多不可显示的控制符(比如NULL、换行符之类的),并且第127个是删除符,所以这一步将数字范围控制在了64~126之间。 -
将数字转化成ASCII表中对应的字符:
txt
`~oia@
综上,一个浮点数被编码成了一串字符串,并且自带分隔符。对于时间戳这样的长整型来说,不进行第1步也可以编码为一串字符串,那么,将经度、纬度和时间戳的编码拼接起来就能够组成一个轨迹点的编码,还不用额外的分隔符。 其实如果单独对一个数字来说,这个方法的压缩率并不高,有时候还会得不偿失。比如上述的数字-179.9832104
,使用这种方法压缩后成为5个字符,占5个字节。如果将直接乘以 <math xmlns="http://www.w3.org/1998/Math/MathML"> 1 0 5 10^5 </math>105,可以直接用有符号的整型表示,还只占4个字节呢!所以,这个算法还有一个思想就是:后一个值用其与前一个值的差值表示,由于差值比较小,所以在上述的第5步时可以将高位的零舍弃掉,节省空间。下面是Google地图开发平台中给出的例子(这个例子中没有带时间戳):

可以看到,这个例子中除了第一个轨迹点存储时全量信息(完整的经纬度数据)之外,后面的轨迹点都是存储的相对于前一个轨迹点偏移量。
解码
解码其实就比较简单了,相当于把编码步骤倒过来执行一遍。编码中的步骤4可以使得我们在解码时知道这个数字是正数还是负数,步骤7可以让我们知道数字的边界在哪,所以还是比较方便解码的,我在此就不再赘述解码过程了。
Talk is cheap, show me the code
原理知道了之后,代码就比较简单了,贴一个我写的代码: SimpleTrajectoryCompressor