早在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更新為新型緊湊型切片格式。