前言
做任何事情都不是想象中的那么簡單。好久沒有更新技術博客了,跟最近瞎忙有很大關系,雖說是瞎忙也抽空研究了些技術。
主要是前端渲染,像原生的WebGL和Cesium。WebGL寫了幾篇博客,自我感覺還可以。Cesium是一個封裝好的WEB端3D Earth框架,有了WebGL的基礎之后切換到Cesium按理說一切應該是順理成章,簡單的測試了幾個功能之后發現確實非常好,簡單的幾行代碼就可以實現Google Earth的功能,當然Google Earth重要的絕對不是他的渲染框架。
前期做了很多Geotrellis的工作,那么我就想着能不能把Geotrellis發布的TMS加載到Cesium中來,本來這是很簡單的嘛,以前是在leaft-let中顯示,現在就是換一個地方顯示而已,並且Cesium已經調通。說干就干,結果怎么着,前天晚上整到四點,昨天折騰了幾個小時居然一直不出圖,所以我說任何看似簡單的事情其實都不簡單,下面就讓我娓娓道來。
一、Cesium
1.1 簡介
介紹之前還是來簡單介紹一下Cesium,當然如果后面繼續對此框架進行研究的話可能也會多寫幾篇關於此框架的博客。
官網地址:https://cesiumjs.org/,Github地址:https://github.com/AnalyticalGraphicsInc/cesium。
其功能簡單明了,當然也很強大,基礎教程可以參考http://blog.csdn.net/UmGsoil/article/category/7005304,當然官方文檔更好。
1.2 簡單使用
無需考慮這么復雜,從簡單里說Cesium就是一個前端地圖渲染引擎,與leaft-let、OpenLayer相同,只是Cesium做成了3D的。所以從基礎功能都是相似的。
首先在html頁面加載Cesium,如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Hello 3D Earth</title>
<script src="CesiumUnminified/Cesium.js"></script>
<style>
@import url(CesiumUnminified/Widgets/widgets.css);
html, body, #cesiumContainer {
width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden;
}
</style>
</head>
<body>
<div id="cesiumContainer"></div>
<script src="my_js.js"></script>
</body>
</html>
其中CesiumUnminified存儲了相關文件,從Github中下載即可。my_js.js
是我們自己要寫的js文件。my_js.js最簡單的情況只需要一句話即可:
var viewer = new Cesium.Viewer("cesiumContainer");
這樣瀏覽器就會渲染出一個3維地球並自動加載微軟的影像地圖。那么如何更改或者添加圖層呢?
var viewer = new Cesium.Viewer("cesiumContainer", {
animation: true, //是否顯示動畫控件(左下方那個)
baseLayerPicker: false, //是否顯示圖層選擇控件
geocoder: true, //是否顯示地名查找控件
timeline: true, //是否顯示時間線控件
sceneModePicker: true, //是否顯示投影方式控件
navigationHelpButton: false, //是否顯示幫助信息控件
infoBox: true, //是否顯示點擊要素之后顯示的信息
imageryProvider : new Cesium.WebMapTileServiceImageryProvider({
url: "http://t0.tianditu.com/vec_w/wmts?service=wmts&request=GetTile&version=1.0.0&LAYER=vec&tileMatrixSet=w&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default&format=tiles",
layer: "tdtVecBasicLayer",
style: "default",
format: "image/jpeg",
tileMatrixSetID: "GoogleMapsCompatible",
show: false
}),
geocoder: false // no default bing maps
});
//全球影像中文注記服務
viewer.imageryLayers.addImageryProvider(new Cesium.WebMapTileServiceImageryProvider({
url: "http://t0.tianditu.com/cia_w/wmts?service=wmts&request=GetTile&version=1.0.0&LAYER=cia&tileMatrixSet=w&TileMatrix={TileMatrix}&TileRow={TileRow}&TileCol={TileCol}&style=default.jpg",
layer: "tdtAnnoLayer",
style: "default",
format: "image/jpeg",
tileMatrixSetID: "GoogleMapsCompatible",
show: false
}));
這段代碼就會自動在3維地球中加載天地圖的線划圖並添加注記。所以剩下的事情就很簡單了,只需要再添加我自己的TMS即可。
1.3 問題來了
在上述代碼下方添加如下代碼:
var layers = viewer.scene.imageryLayers; //所有圖層(非基本圖層)
var layer = layers.addImageryProvider(
new Cesium.UrlTemplateImageryProvider({
url : 'http://xxxx/modis/ndvi/{z}/{x}/{y}',
format: "image/png"
})
);
//50%透明度
layer.alpha = 0.5;
//兩倍亮度
layer.brightness = 2.0;
很簡單的代碼,獲取圖層對象,然后添加一層,url為我們自己的瓦片請求格式,這是我用Geotrellis發布的modis數據ndvi服務。並設置該圖層透明度和增加亮度防止蓋住上面的注記層。本來應該是點擊一下刷新就出來效果的事情,結果足足折騰到我崩潰。
無論怎么刷新就是出不來那層瓦片,其他兩層數據正常顯示,打開瀏覽器的調試模式,能夠看到對ndvi瓦片的請求返回的都是200 OK,也能在調試中看到單個瓦片應有的效果。然后變換各種添加圖層的格式(UrlTemplateImageryProvider、WebMapTileServiceImageryProvider、Cesium.createTileMapServiceImageryProvider這些不是本文重點,在后續文章詳細介紹)均顯示不出瓦片,而后又去掉其他兩層瓦片只保留NDVI,最后又添加Geotrellis發布的其他TMS服務,但是無論怎么折騰,只要是我自己Geotrellis發布的TMS均無法顯示,折騰到四點多,始終沒有出來,在stackoverflow和github上提了問,等了半天也無人回復,只好悶悶不樂的去睡了。
二、解決方案
2.1 轉角遇到答案
今天中午小睡片刻,起床后收到一封郵件,趕緊打開看了一下,是Github的回復郵件,喜出望外,結果一看內容原來是告訴我不要在Issue中發布提問,告訴了我Google的提問列表(https://groups.google.com/forum/#!msg/cesium-dev/RfAlZZkPBaM/xGOK01trAwAJ;context-place=forum/cesium-dev),整個人當時就不好了,既然這樣只有上去瞅瞅,打開簡單一搜索,居然有現成的,問題描述跟我的一模一樣,解決方案是添加CORS。
其實我之前折騰到四點多的時候腦子里就有這個意識,一定是我發布的TMS缺少了某個東西(或者是某個東西與Cesium的要求不一致),導致Cesium無法正常顯示我的瓦片,所以一看到這個我就亢奮了,程序員的直覺告訴我這肯定就是我要找的東西。
2.2 解決
所以問題就來了,看樣子我要在Geotrellis中折騰CORS了。Geotrellis采用Scala語言開發,所以我也是拿Scala寫的,發布網絡服務用的是Akka,Akka是開源的網絡服務框架,於是就搜索了一下Akka CORS,很快就有了答案。
關於CORS的介紹,看這篇文章就夠了:http://www.ruanyifeng.com/blog/2016/04/cors.html。CORS簡單來說就是跨域資源共享,當跨域進行Ajax請求的時候進行權限驗證等操作。其實細細想來倒是這么回事,Cesium請求瓦片一定用的是XMLHttpRequest,而我的TMS又未使用CORS,於是怎么折騰都出不來結果,當然對這塊不太熟悉是導致問題發生的直接原因。
找到問題解決就很容易了,Github中有現成的解決方案。首先添加一個CorsSupport特質,如下:
import akka.http.scaladsl.model.HttpHeader
import akka.http.scaladsl.model.HttpMethods._
import akka.http.scaladsl.model.HttpResponse
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.model.headers.Origin
import akka.http.scaladsl.server.Directive0
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.MethodRejection
import akka.http.scaladsl.server.RejectionHandler
trait CorsSupport {
protected def corsAllowOrigins: List[String]
protected def corsAllowedHeaders: List[String]
protected def corsAllowCredentials: Boolean
protected def optionsCorsHeaders: List[HttpHeader]
protected def corsRejectionHandler(allowOrigin: `Access-Control-Allow-Origin`) =
RejectionHandler
.newBuilder().handle {
case MethodRejection(supported) =>
complete(HttpResponse().withHeaders(
`Access-Control-Allow-Methods`(OPTIONS, supported) ::
allowOrigin ::
optionsCorsHeaders
))
}
.result()
private def originToAllowOrigin(origin: Origin): Option[`Access-Control-Allow-Origin`] =
if (corsAllowOrigins.contains("*") || corsAllowOrigins.contains(origin.value))
origin.origins.headOption.map(`Access-Control-Allow-Origin`.apply)
else
None
def cors[T]: Directive0 = mapInnerRoute { route => context =>
((context.request.method, context.request.header[Origin].flatMap(originToAllowOrigin)) match {
case (OPTIONS, Some(allowOrigin)) =>
handleRejections(corsRejectionHandler(allowOrigin)) {
respondWithHeaders(allowOrigin, `Access-Control-Allow-Credentials`(corsAllowCredentials)) {
route
}
}
case (_, Some(allowOrigin)) =>
respondWithHeaders(allowOrigin, `Access-Control-Allow-Credentials`(corsAllowCredentials)) {
route
}
case (_, _) =>
route
})(context)
}
}
爾后在發布TMS服務的類中實現該特質:重寫虛方法,並在原先發布TMS服務的地方將原結果傳入cors方法:
override val corsAllowOrigins: List[String] = List("*")
override val corsAllowedHeaders: List[String] = List("Origin", "X-Requested-With", "Content-Type", "Accept", "Accept-Encoding", "Accept-Language", "Host", "Referer", "User-Agent")
override val corsAllowCredentials: Boolean = true
override val optionsCorsHeaders: List[HttpHeader] = List[HttpHeader](
`Access-Control-Allow-Headers`(corsAllowedHeaders.mkString(", ")),
`Access-Control-Max-Age`(60 * 60 * 24 * 20), // cache pre-flight response for 20 days
`Access-Control-Allow-Credentials`(corsAllowCredentials)
)
def service = cors {
pathPrefix("map") {
...
}
}
注意此處的cors方法,其本身是一個無參數方法,此處傳入的是Directive0的apply方法的參數,所以返回的仍然是Route類型。
上述兩段代碼實現的就是將TMS服務實現CORS服務。請求的域為*,即任何域都可;請求頭為"Origin", "X-Requested-With", "Content-Type", "Accept", "Accept-Encoding", "Accept-Language", "Host", "Referer", "User-Agent";並支持發送cookie等認證。完成上述改造后重新編譯運行geotrellis程序,刷新瀏覽器即可看到我們想要的結果,效果如下:
三、總結
本文簡單記錄了將Cesium和Geotrellis結合中碰到的一個小問題,只是剛開始,后續估計問題會更多,無他法,只能咬着牙往下走。結果很簡單,折騰的時間卻很長,但是不折騰肯定是不會有結果的,只能是想辦法加快折騰的速度。當然有些東西一定會記得你的折騰,比如腰椎頸椎當然還有大腦,在折騰中你會對整體框架更加熟悉。
Geotrellis系列文章鏈接地址http://www.cnblogs.com/shoufengwei/p/5619419.html