早在2010年年底,牛魔王中王在其博客空间牛魔王的作坊中对ArcGIS 10中推出的紧凑型缓存格式进行了详细的解读,详见《ArcGIS 切片缓存紧凑文件格式分析与使用》。紧随着的4年时间里,ArcGIS for Server本身经历了10、10.1.X和10.2.X各版本的逐级更替,特别是软件架构发生了显著的变化。然而,就紧凑型缓存本身而言,牛魔王中王的解读一直都是适用的。衷心地向我们的大牛致敬!
直到2014年年底ArcGIS 10.3正式发布,Esri才推出了新的紧凑型缓存格式以增强用户的访问体验。新的缓存格式下,关键的差别在于Esri将缓存的索引信息.bundlx包含在了缓存的切片文件.bundle中。
接下来,我们就简单解读一下这一新型的紧凑型缓存格式。俗语说,万变不离其宗。既然缓存文件夹下仅包含了bundle文件,可以想见,切片的索引,切片的偏移和切片的图片流都必然包含在这一文件中。根据经验,缓存本身遵循的是16进制的形式。依照这一思路,利用UltraEdit打开bundle文件并以16进制格式进行查看。
为了便于分析,我们先创建一个在L00级只包含一个切片的缓存服务,并在UltraEdit中以16进制格式查看L00级下的R0000C0000.bundle文件。
通过对这一文件中信息存储规律的分析,可初步得出如下结论:(1) 文件中包含大量04 00 00 00 00 00 00 00的16进制字节组,共计16893组;(2) 文件中仅包含一个PNG24的文件头字节组89504E47,即第一行第一列的切片,bundle文件中唯一的一张图片。图片流紧随(1)中所提到的字节组之后,但偏移4个字节;(3) (2)中所述的4字节偏移量的数值恰等于图片流的长度;(4) 文件第5行的起始4个字节44 00 02 00按照低位到高位换算出的数值等于131140,这一值与(2)中所述的PNG文件头位置恰恰吻合。
综上分析:(1)起始4行是bundle的文件头信息,可忽略;(2)bundle的文件头之后记录了16384张切片的切片位置,仅4字节,从低位到高位,后4字节可忽略;(3)位置信息之后,对于切片的记录,先以4字节记录切片的长度,而后紧跟图片流信息。到此,bundle结束。
下一步呢,我们将选择一个狭长的矩形面要素发布服务并切图,以分析行列切片在bundle文件中的具体存储规律。
通过对bundle文件和对应的松散缓存在L02级别上的对比,可推断:(1)bundle中索引的存储是按行依次存储,即第1行的1至128,第2行的1至128,以此类推,直至最后一张切片即第128行128列;(2)bundle中图片流的存储仅包含非空切片。此外,通过对这一更复杂的地图缓存的分析,再次论证了前面的推论。
既然上述的分析完毕,接下来就要对上述的分析进行一番验证啦。这里呢,我会利用ArcGIS Runtime SDK for Android实现抽象理论的实践工作。本次验证的核心在于,通过对TiledServiceLayer进行扩展,按照上面的存储推论覆写getTile(int mLevel, int mColumn, int mRow)方法。
第一步,根据参数中的比例级别、列号和行号定位到Bundle文件。
1 String level = Integer.toString(mLevel); 2 int levelLength = level.length(); 3 if(levelLength == 1){ 4 level = "0" + level; 5 } 6 level = "L" + level; 7 8 int rowGroup = 128*(mRow/128); 9 String row = Integer.toHexString(rowGroup); 10 int rowLength = row.length(); 11 if(rowLength < 4){ 12 for(int i=0; i<4-rowLength; i++){ 13 row = "0" + row; 14 } 15 } 16 row = "R" + row; 17 18 int columnGroup = 128*(mColumn/128); 19 String column = Integer.toHexString(columnGroup); 20 int columnLength = column.length(); 21 if(columnLength < 4) { 22 for(int i=0; i<4-columnLength; i++){ 23 column = "0" + column; 24 } 25 } 26 column = "C" + column; 27 28 String bundleName = String.format("%s/%s/%s%s", compactTileLoc, level, row, column) + ".bundle";
第二步,读取bundle文件,根据前面分析中所推断出的切片的起始位置和切片的长度获取对应的切片并返回。
1 int index = 128*(mRow - rowGroup) + (mColumn-columnGroup); 2 3 RandomAccessFile isBundle = new RandomAccessFile(bundleFileName, "r"); 4 isBundle.skipBytes(64 + 8*index); 5 6 //获取位置索引并计算切片位置偏移量 7 byte[] indexBytes = new byte[4]; 8 isBundle.read(indexBytes, 0, 4); 9 long offset = (long)(indexBytes[0]&0xff) +(long)(indexBytes[1]&0xff)*256 + (long)(indexBytes[2]&0xff)*65536 10 + (long)(indexBytes[3]&0xff)*16777216; 11 12 //获取切片长度索引并计算切片长度 13 long startOffset = offset - 4; 14 isBundle.seek(startOffset); 15 byte[] lengthBytes = new byte[4]; 16 isBundle.read(lengthBytes, 0, 4); 17 int length = (int)(lengthBytes[0] & 0xff) + (int)(lengthBytes[1] & 0xff)*256 + (int)(lengthBytes[2] & 0xff) * 65536 18 + (int)(lengthBytes[3] & 0xff) * 16777216; 19 20 //根据切片位置和切片长度获取切片 21 ByteArrayOutputStream bos = new ByteArrayOutputStream(); 22 23 byte[] tileBytes = new byte[length]; 24 int bytesRead = 0; 25 if(length > 0){ 26 bytesRead = isBundle.read(tileBytes, 0, tileBytes.length); 27 if(bytesRead > 0){ 28 bos.write(tileBytes, 0, bytesRead); 29 } 30 } 31 32 tile = bos.toByteArray();
呵呵,成功实现。直接奉上Android端的显示效果吧。
Tips:
关于紧凑型切片,唠唠叨叨的我还是忍不住要嘱咐几句:(1)新型的紧凑型切片无法被直接用于先前版本的ArcGIS for Server;(2)新型的紧凑型切片可通过导出切片即export tiles获取先前格式的缓存;(3)老版本的紧凑型切片可直接在新版本的ArcGIS for Server中复用,但可通过升级存储格式即Upgrade Storage Format更新为新型紧凑型切片格式。