前言
在上兩篇文章中我介紹了如何直接將Geotiff(一個或者多個)發布為TMS服務。這中間其實我遇到了一個問題,並且這個問題伴隨Geotrellis的幾乎所有使用案例,下面我詳細講述。
一、問題描述
無論在將Tiff文件使用Geotrellis導入Accumulo中還是直接將其發布為TMS服務,其實這中間都存在一個問題:當多個Tiff文件存在重疊部分的時候如何接邊、去重疊以及在邊界處的瓦片如何取出各Tiff文件中涉及到的數據,即保持瓦片顯示效果的完整性。
這個問題可以說是一個問題也可以說是兩個問題。當我們采用事先導入Accumulo中的方案的時候,這個問題不存在(下面會介紹不存在的原因),這也是我一直沒有理會此問題的原因,而當我們直接加載Tiff文件為TMS服務的時候這個問題便出現了,當某一個瓦片對應的是兩個或者多個Tiff文件的時候,無論選用哪一個文件都會導致最終的瓦片數據不全。接下來我們就來解決這個問題。
二、原理分析及最終效果
要解決這個問題,首先要搞明白Geotrellis是如何讀取Tiff文件的。
Geotrellis使用HadoopGeoTiffRDD類將Tiff文件直接讀取為RDD,主要方法如下:
def apply[I, K, V](path: Path, uriToKey: (URI, I) => K, options: Options)(implicit sc: SparkContext, rr: RasterReader[Options, (I, V)]): RDD[(K, V)] = {
val conf = configuration(path, options)
options.maxTileSize match {
case Some(tileSize) =>
val pathsAndDimensions: RDD[(Path, (Int, Int))] =
sc.newAPIHadoopRDD(
conf,
classOf[TiffTagsInputFormat],
classOf[Path],
classOf[TiffTags]
).mapValues { tiffTags => (tiffTags.cols, tiffTags.rows) }
apply[I, K, V](pathsAndDimensions, uriToKey, options)
case None =>
sc.newAPIHadoopRDD(
conf,
classOf[BytesFileInputFormat],
classOf[Path],
classOf[Array[Byte]]
).mapPartitions(
_.map { case (p, bytes) =>
val (k, v) = rr.readFully(ByteBuffer.wrap(bytes), options)
uriToKey(p.toUri, k) -> v
},
preservesPartitioning = true
)
}
}
其中path為傳入的路徑,所以configuration方法是關鍵,其定義如下:
private def configuration(path: Path, options: Options)(implicit sc: SparkContext): Configuration = {
val conf = sc.hadoopConfiguration.withInputDirectory(path, options.tiffExtensions)
conf
}
withInputDirectory方法定義如下:
def withInputDirectory(path: Path, extensions: Seq[String]): Configuration = {
val searchPath = path.toString match {
case p if extensions.exists(p.endsWith) => path
case p =>
val extensionsStr = extensions.mkString("{", ",", "}")
new Path(s"$p/*$extensionsStr")
}
withInputDirectory(searchPath)
}
def withInputDirectory(path: Path): Configuration = {
val allFiles = HdfsUtils.listFiles(path, self)
if(allFiles.isEmpty) {
sys.error(s"$path contains no files.")
}
HdfsUtils.putFilesInConf(allFiles.mkString(","), self)
}
看完這兩個方法我們是不是就豁然開朗了。extensions是一個Tiff文件擴展名的集合。當此文件是tiff文件的時候就直接讀取他,如果不是的時候就以他為文件夾讀取他下面的所有可能tiff文件,所以我上面說事先導入Accumulo時該問題不存在,因為那時我傳入的正是文件夾,系統直接幫我達到了想要的結果。
所以我們就可以明白,如果你想讓Geotrellis處理接邊、重疊等問題可以直接傳入包含所有要處理的Tiff數據的文件夾,這樣系統就會自動處理(具體的接邊、去重疊操作在rdd的union方法中,由UnionRDD類具體完成)。
所以我們的Tiff文件直接發布為TMS也可以才用同樣的處理方式,只需要將文件夾傳入即可,這樣在涉及到重疊、接邊區域的瓦片還能保證數據的完整性。但是這樣又出現了另一個問題,如果一次讀入所有文件勢必會造成處理速度很慢,那么我們為什么不能只取出當前瓦片涉及到的文件呢,如果只涉及一個Tiff就取一個,如果涉及到多個Tiff就取多個。這樣既不會造成數據缺失也不會造成讀取緩慢。先來看一下最終效果。
我下載了14幅連續的srtm數據,采用上述方式發布的TMS最終結果如下圖:
從中可以看出拼接的效果非常好,如果是只讀取單幅Tiff的情況必然兩幅之間會存在空白,采用這種逐一讀取的方式,不僅結果完美,效率也較高。下面來介紹實現方案。
三、實現方案
整體實現方案如下:
- 判斷並取出與請求的瓦片有交集的Tiff文件
- 將這些Tiff文件作為整體讀取rdd並發布TMS
3.1 判斷並取出與請求的瓦片有交集的Tiff文件
上一篇文章中已經大致介紹了此塊內容,在這里再簡要介紹一下更新后的版本,代碼如下:
val files = HdfsUtils.listFiles(new Path(self.getHdfsUri), sc.hadoopConfiguration)
files.filter { s =>
if (HadoopGeoTiffRDD.Options.DEFAULT.tiffExtensions.exists(s.toString.endsWith)) {
val tiffExtent = getExtent(s)
extent.intersects(tiffExtent)
}
else
false
}
def getExtent(path: Path) = {
val rdd = HadoopGeoTiffRDD.spatialMultiband(path)
rdd
.map { case (key, grid) =>
val ProjectedExtent(extent, crs) = key.getComponent[ProjectedExtent]
// Bounds are return to set the non-spatial dimensions of the KeyBounds;
// the spatial KeyBounds are set outside this call.
val boundsKey = key.translate(SpatialKey(0,0))
val cellSize = CellSize(extent, grid.cols, grid.rows)
HashMap(crs -> RasterCollection(crs, grid.cellType, cellSize, extent, KeyBounds(boundsKey, boundsKey), 1))
}
.reduce { (m1, m2) => m1.merged(m2){ case ((k,v1), (_,v2)) => (k,v1 combine v2) } }
.values.toSeq.head.extent
}
其中sc為SparkContext對象,hdfsPath為HDFS中Tiff文件夾路徑,files即為此文件下面所有文件,getExtent函數獲取傳入Tiff文件的空間范圍。filter操作過濾掉非Tiff文件以及與extent(瓦片的空間范圍)不相交的Tiff文件。這樣就可以得到所有與此瓦片有關的Tiff文件。
3.2 將這些Tiff文件作為整體讀取rdd並發布TMS
此步又有兩種思路(原諒我最近中國哲學簡史看多了,總想往高大上的哲學上套一套):
- 將這些Tiff文件作為整體提交Geotrellis得到最終結果
- 讀取每一幅tiff文件然后手動union
兩種方案各有利弊,第一種需要自己寫讀取多個Tiff文件的方案,第二種需要我們手動union,在這里我都介紹一下。
3.2.1 讀取多個Tiff文件
解決思路就是將多個Tiff文件提交到上述的conf中,這樣系統就會自動幫我們讀取。簡單的說就是改寫上述configuration函數。代碼如下:
private def configuration(paths: Array[Path])(implicit sc: SparkContext): Configuration = {
val union = paths.mkString(",")
HdfsUtils.putFilesInConf(union, sc.hadoopConfiguration)
}
其中paths為上一步獲取到的與瓦片有關的Tiff文件集合。只需要重寫上面的讀取Tiff的apply方法,將其中的configuration換成此函數即可。
3.2.2 逐一讀取,手動union
思路清晰明了,代碼如下:
paths.map(s => HadoopGeoTiffRDD.spatial(s))
.reduce((a, b) => a union b)
很簡單的代碼,先對Tiff文件集合進行map操作讀取所有rdd,然后執行reduce操作,reduce執行的函數為union,即將兩個rdd聯合,意味着拼接和去重疊。這樣也可解決問題。
四、總結
本文簡單講述了使用Geotrellis處理Tiff文件時的兩個細節,通過這兩個細節能夠讓我們對Geotreliis的核心更加了解,也能夠使我們更加便捷和靈活的處理實際中碰到的關於數據方面的問題。
Geotrellis系列文章鏈接地址http://www.cnblogs.com/shoufengwei/p/5619419.html