本文只介紹實際工作中常用的 WMS、WMTS、WFS、TMS 四種,WCS、WPS 等其它 OGC WebService 型別請自行查閱官方資料。
三種方式對於下文列舉的服務並不是全都存在的,例如 WMS 就只有第一種。
本文不介紹 SOAP 方式(因為太複雜了)。
以 1.1.0
版本為參考。
GetCapabilities
GetMap
GetFeatureInfo
以這樣一個請求地址為例:
http://localhost:4800/geoserver/spatial_base/wms?<queryString>
queryString
即查詢字串,我把它列成表格:
param | value | desc |
---|---|---|
service | WMS | 服務型別 |
version | 1.1.0 | 服務版本 |
request | GetMap | 方法 |
layers | spatial_base:guangxi_cities | 哪個圖層 |
bbox | 104.450889587402,20.8992862701416,112.061851501465,26.3855667114258 | 要多大範圍 |
width | 768 | 要返回的影象畫素寬 |
height | 553 | 要返回的影象畫素高 |
srs | EPSG:4326 | 用哪個座標系 |
styles | "" | 用什麼樣式,預設要給空字串 |
format | image/png | 格式 |
GeoServer 的 layers 引數,是「工作空間名:圖層名」這樣的組合。
那麼,它返回的就是一張圖:
這是最常規的使用「鍵值對」,也就是 queryString 來請求地圖的 WMS 用法。
在 CesiumJS 中:
new Cesium.WebMapServiceImageryProvider({
url: 'http://localhost:4800/geoserver/spatial_base/ows',
layers: 'spatial_base:guangxi_cities',
parameters: {
transparent: true,
format: 'image/png',
},
})
只需要保證座標系合適即可,當然,url 的 ows
也可以改為 wms
。
OWS 只是 GeoServer 上的一個萬用字元,如果你知道你想用的是什麼服務,可以不寫 ows,例如本例,可以直接寫 'http://localhost:4800/geoserver/spatial_base/wms'
在 OpenLayers6 中:
import TileLayer from 'ol/layer/Tile'
import TileWMS from 'ol/source/TileWMS'
new TileLayer({
extent: [104.4509, 20.8993, 112.0619, 26.3856], // 座標系保持與 View 一致
source: new TileWMS({
url: 'http://localhost:4800/geoserver/spatial_base/wms',
params: {
'LAYERS': 'spatial_base:guangxi_cities',
},
serverType: 'geoserver', // 有好幾種地理伺服器,要明確指定
}),
})
// View 座標系的設定
import { View } from 'ol'
new View({
// ...
projection: get('EPSG:4326') // 返回一個 Projection 範例即可
})
根據 OpenLayers 的檔案:
At least a
LAYERS
param is required.STYLES
is''
by default.VERSION
is1.3.0
by default.WIDTH
,HEIGHT
,BBOX
andCRS
(SRS
for WMS version < 1.3.0) will be set dynamically.
也就是至少要設定 LAYERS
引數。如果請求的圖層的座標系與 View 的一致,則不需要設定 SRS
。
WMS 雖然主要的用途是請求地圖圖片,是一種經典的伺服器渲染服務,但是也保留了基本的要素查詢功能,也就是 GetFeatureInfo
,舉例:
http://localhost:4800/geoserver/spatial_base/ows
?service=WMS
&version=1.1.1
&request=GetFeatureInfo
&layers=spatial_base:guangxi_cities
&bbox=101.25,22.5,112.50,33.75
&width=256
&height=256
&srs=EPSG:4326
&query_layers=spatial_base:guangxi_cities
&info_format=application/json
&x=181
&y=199
GetFeatureInfo
是一個可選的功能,GeoServer 有這個功能。簡單解釋一下,引數 width
和 height
是引數 bbox
範圍生成的一小塊圖片,然後去查詢這塊圖片中畫素位置是 x
、y
的要素資訊,其它引數不難理解。
WMTS 的軸朝向如下圖所示。
GetCapabilities
(獲取WMTS後設資料檔案,也叫獲取能力檔案)GetTile
(獲取一張瓦片)GetFeatureInfo
(可選能力)WMTS 的行列號、瓦片陣(TileMatrix,類似層級的概念,參考第 5 節)是從 0 開始算的,例如 TileMatrix=EPSG:4326:0&TileCol=0&TileRow=0
。
以 2.3 小節中的「TileRow=55 & TileCol=103」這張瓦片為例:
它的請求地址是:
http://localhost:4800/geoserver/gwc/service/wmts?layer=spatial_base%3Aguangxi_cities&style=&tilematrixset=EPSG%3A900913&Service=WMTS&Request=GetTile&Version=1.0.0&Format=image%2Fpng&TileMatrix=EPSG%3A900913%3A7&TileCol=103&TileRow=55
把 queryString 列成表格即:
param | value | desc |
---|---|---|
layer | spatial_base:guangxi_cities | 與 WMS 的 layers 意義一致 |
style | "" | 與 WMS 的 style 意義一致 |
tilematrixset | EPSG:900913 | 瓦片陣集,見本文第5節 |
TileMatrix | EPSG:900913:7 | 當前級別的瓦片陣,見本文第5節 |
TileCol | 103 | 瓦片列號 |
TileRow | 55 | 瓦片行號 |
Service | WMTS | 與 WMS 的 service 意義一致 |
Request | GetTile | 與 WMS 的 request 意義一致 |
Version | 1.0.0 | 與 WMS 的 version 意義一致 |
Format | image/png | 與 WMS 的 format 意義一致 |
聰明的你應該注意到了,引數的名稱是大小寫任意的,但是引數值部分大小寫敏感,例如 「EPSG:900913」寫成「epsg:900913」是請求不到的(至少 GeoServer 是這樣);但是「GetTile」寫成「gettile」又是可以的。
圖與上一小節返回的是一樣的,使用 RESTful 風格的請求是這樣的:
http://localhost:4800/geoserver/gwc/service/wmts/rest/spatial_base:guangxi_cities/polygon/EPSG:900913/EPSG:900913:7/55/103?format=image/png
ResourceURL
標籤,此處的地址是 REST 風格的。在 CesiumJS 中,你要十分小心每一級「TileMatrix」的名稱,因為 CesiumJS 使用的是 REST 風格的請求地址,這就意味著,TileMatrix 必須與能力檔案中對應圖層的 TileMatrix 名稱一致,才能拼湊出正確的 URL。
CesiumJS 預設 WMTS 每一級的 TileMatrix 名稱就是簡單的「0、1、2、3、4...」,但是 GeoServer 預設的 900913 和 4326 這兩個 TileMatrixSet
的名稱卻是 「EPSG:4326:0、EPSG:4326:1、EPSG:4326:2...」和「EPSG:900913:0、EPSG:900913:1、EPSG:900913:2...」,面對這種情況也好辦,我們可以用 JavaScript 快速生成這樣一個有規律的 TileMatrixLabels
陣列:
const maxLevel = 3 // 別忘了改成你的 WMTS 支援的最大等級,此處演示寫個 3
const tileMatrixID = 'EPSG:900913'
const tileMatrixLabels = Object.keys(new Array(maxLevel).fill(0)).map(v => `${tileMatrixID}:${v}`)
// tileMatrixLabels 即 ['EPSG:900913:0', 'EPSG:900913:1', 'EPSG:900913:2']
現在,你可以用這個 tileMatrixLabels
陣列建立 WebMapTileServiceImageryProvider
:
new Cesium.WebMapTileServiceImageryProvider({
url: 'http://localhost:4800/geoserver/gwc/service/wmts/rest/spatial_base:guangxi_cities/{style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}?format=image/png',
style: 'polygon', // 改 'default' 就是預設的樣式
layer: 'spatial_base:guangxi_cities',
tileMatrixLabels: tileMatrixLabels,
tileMatrixSetID: 'EPSG:900913',
rectangle: Cesium.Rectangle.fromDegrees(104.450889587402,20.8992862701416,112.061851501465,26.3855667114258),
})
你甚至可以封裝一個函數獲取這些 TileMatrix 的名稱:
/**
* 建立 TileMatrix 的名稱
* @param {string} tileMatrixID 即 `TileMatrixSet` 的名稱
* @param {number} maxLevel 即 WMTS 的最大等級
* @returns {string[]}
*/
const createTileMatrixLabels = (tileMatrixID, maxLevel) =>
Object.keys(new Array(maxLevel).fill(0)).map(v => `${tileMatrixID}:${v}`)
至於 OpenLayers6 載入 WMTS,也是需要計算解析度、TileMatrixIDs 的,略嫌麻煩,以 EPSG:4326
為例,你需要計算圖層的解析度列表、TileMatrix 名稱列表:
// 計算 22 級別,夠用就行
const resolutions = new Array(22)
// EPSG:4326 一級寬度跨 2 個 256 畫素的瓦片
const firstLevelPixelWidth = 360 / (256 * 2)
for (let z = 0; z < 22; ++z) {
// 逐級除以 2
resolutions[z] = firstLevelPixelWidth / Math.pow(2, z)
}
const tileMatrixLabels = createTileMatrixLabels('EPSG:4326', 22)
隨後,你就可以用 resolutions
和 tileMatrixLabels
建立一個 WMTSTileGrid
,進而建立 WMTS 圖層了:
const wmtsTileGrid = new WMTSTileGrid({
origin: [-180, 90], // origin 即當前座標系的的左上角
resolutions: resolutions,
matrixIds: tileMatrixLabels,
})
new TileLayer({
extent: [104.4509, 20.8993, 112.0619, 26.3856],
source: new WMTS({
// 和 CesiumJS 不太一樣,這裡不用到模板那麼細
url: 'http://localhost:4800/geoserver/gwc/service/wmts',
layer: 'spatial_base:guangxi_cities',
matrixSet: 'EPSG:4326', // 與能力檔案中此圖層的 TileMatrixSet 一致
format: 'image/png',
// 也可以用 `ol/proj` 包匯出的 get('EPSG:4326'),返回 Projection 範例即可
projection: 'EPSG:4326',
tileGrid: wmtsTileGrid,
style: 'polygon',
wrapX: true,
}),
})
而如果是 EPSG:3857
,也即 EPSG:900913
,那麼你的 origin
、extent
、firstLevelPixelWidth
就要隨之改變了:
const origin = [-20037508.34, 2003708.34]
const extent = [11627419.84177403,2379873.5953122815,12474668.246494522,3046913.9333698303]
const firstLevelPixelWidth = 40075016.68557849 / 256
至於為什麼是這幾個數位,請檢視 GeoServer 中相關 Gridset 的數值吧,需要有 Web 墨卡託座標系相關的基礎。
TMS 是一種非常接近靜態資源的地圖瓦片資料集,常見的瓦片格式有 jpeg
、png
、pbf
等。這個標準比較舊了,但是勝在簡單。
TMS 軸朝向如下圖所示。
一般來說,TMS 的地址會指向一個名稱是 tilemapresource.xml
的檔案。當然,GeoServer 就比較例外,僅僅是返回 XML 檔案而地址並不指向 XML 檔案。
這個 XML 檔案是 TMS 最顯著的特徵,記錄了這個瓦片地圖集的後設資料:
<TileMap version="1.0.0" tilemapservice="...">
<!-->...<-->
</TileMap>
請求地址:
http://localhost:4800/geoserver/gwc/service/tms/1.0.0
/spatial_base:guangxi_cities@EPSG:900913@png
/7/103/72.png
如下圖所示:
新增一個 XYZ 圖層即可,但是在模板連結中要填寫的是 {z}/{x}/{-y}.imageExt
,而不是 {z}/{x}/{y}.imageExt
。
CesiumJS 在測試中發現對非全範圍的 EPSG4326 或 3857 座標系的 TMS 載入是存在問題的。GeoServer 釋出的圖層一般沒有這個問題。
舉例:
new Cesium.TileMapServiceImageryProvider({
url: "http://localhost:4800/geoserver/gwc/service/tms/1.0.0/spatial_base%3Aguangxi_cities@EPSG%3A900913@png",
minimumLevel: 0,
maximumLevel: 15,
rectangle: Cesium.Rectangle.fromDegrees(104.450889587402,20.8992862701416,112.061851501465,26.3855667114258)
})
如果使用的是 TileMapServiceImageryProvider
這個類,那麼它是遵循正確的 z、x、y 順序的。如果使用的是 UrlTemplateImageryProvider
,那麼你需要把模板中的 {y}
改成 {reverseY}
:
new Cesium.UrlTemplateImageryProvider({
url: "http://localhost:4800/geoserver/gwc/service/tms/1.0.0/spatial_base%3Aguangxi_cities@EPSG%3A900913@png/{z}/{x}/{reverseY}.png",
minimumLevel: 0,
maximumLevel: 15,
rectangle: Cesium.Rectangle.fromDegrees(104.450889587402,20.8992862701416,112.061851501465,26.3855667114258)
})
CesiumJS 會判斷預設的 TMS 描述檔案「tilemapresource.xml」,請求失敗則降級成 {z}/{x}/{reverseY}.imageExt
而在 OpenLayers6 中,使用 TMS 則是通過 XYZ
實現的:
new TileLayer({
source: new XYZ({
url: 'http://localhost:4800/geoserver/gwc/service/tms/1.0.0/spatial_base%3Aguangxi_cities@EPSG%3A900913@png/{z}/{x}/{y}.png'
}),
})
這只是最簡單的用法,即預設是 EPSG:3857
或 EPSG:900913
切片方案的 TMS,否則要指定其它的引數,例如 projection
、tileGrid
、tileSize
等,具體參考官方檔案。如果不想控制檯報沒瓦片的地方找不到瓦片的錯誤,則還要加上 extent
引數給 ol/layer/Tile
類:
import TileLayer from 'ol/layer/Tile'
const tmslayer = new TileLayer({
extent: [11627419.84177403,2379873.5953122815,12474668.246494522,3046913.9333698303],
// ...
})
暫時不可以。
參考:Can I use external TMS service as a store in Geoserver?
GeoServer 目前只能掛接其它伺服器的 WMS 和 WMTS,TMS 作為一種比較靜態的瓦片資料服務,建議直接使用 Web 伺服器釋出即可,不需要再經過 GeoServer。
WFS 即使到了 2.0.0 版本,仍然只能用這兩種方式發起請求:
這是有問題的,前端的朋友們熟悉什麼?他們要什麼?JSON 啊!
若不加以限制,WFS 返回的是基於 XML 的 GML 格式。總之,
GetFeature
:獲取向量要素資料DescribeFeatureType
:查詢向量圖層的後設資料GetCapabilities
:獲取能力檔案Transaction
:發起向量圖形互動事務,例如增刪改等這只是主要的能力,2.0.0 還新增了其它的能力,有興趣的可以去看規範,也可以直接參考 GeoServer 的幫助手冊,十分詳細:
我只是好奇,為什麼 GeoServer 本地不能整合一份幫助檔案呢?
這裡以 WFS 2.0.0 為例。
一個最簡單的獲取全部向量要素的請求如下:
http://localhost:4800/geoserver/spatial_base/ows
?service=WFS
&version=2.0.0
&request=GetFeature
&typeNames=spatial_base:guangxi_cities
&outputFormat=application/json
引數 outputFormat
的指定非常重要,若不指定,預設返回的是 GML 格式資料。
幾個常用的引數如下
引數名 | 值型別 | 描述 |
---|---|---|
count | int | 限制返回的個數,取前 count 個 |
featureid | string | 指定要素的 ID 來查詢,格式是「圖層名.id」 |
typeNames | string | 要查詢哪個「圖層」,GeoServer 是「工作空間」:「圖層名」 |
propertyName | string | 需要返回什麼屬性欄位,用英文逗號連線 |
filter | string | 一個 XML 文字,即查詢資訊 |
outputFormat | string | 要返回資料的格式型別,能力檔案中有 |
其中,filter
的 XML 就是 WFS 較為詬病的一點,因為這個 XML 構造起來比較麻煩。
以一個簡單的框選查詢為例,你需要構造如下的 XML:
<Filter xmlns:ogc="http://www.opengis.net/ogc" xmlns:gml="http://www.opengis.net/gml">
<Intersects>
<PropertyName>geom</PropertyName>
<gml:Envelope srsName="EPSG:4326">
<gml:lowerCorner>109 20</gml:lowerCorner>
<gml:upperCorner>120 30</gml:upperCorner>
</gml:Envelope>
</Intersects>
</Filter>
對於我這份資料來說,這樣一個框選查詢能返回 11 個要素。注意,<Intersects>
下的 <PropertyName>
的值,即 "geom",指的是要素圖層的待相交查詢的幾何欄位名稱。如果是 Shapefile,它的幾何欄位可能是 "SHAPE" 或其它。
關於這個 filter
能進行什麼空間查詢,請參考:
上述的 XML 過濾引數,是通過 GET
請求,傳送 QueryString 的方式傳遞到 WFS 的。如果這個過濾的 XML 體積過大,超出了 GET 請求的最大大小(因瀏覽器而異,普遍較小),那麼就需要改成 POST
請求,把這個 XML 作為請求體傳送到伺服器。
請求路徑:
POST http://localhost:4800/geoserver/spatial_base/ows
請求體和上面的 filter
略有不同,要把其它的請求引數也帶上:
<?xml version='1.0' encoding='UTF-8'?>
<wfs:GetFeature
service="WFS"
version="2.0.0"
outputFormat="json"
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:fes="http://www.opengis.net/fes/2.0"
xmlns:gml="http://www.opengis.net/gml/3.2"
xmlns:sf="http://www.openplans.org/spearfish"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/wfs/2.0
http://schemas.opengis.net/wfs/2.0/wfs.xsd
http://www.opengis.net/gml/3.2
http://schemas.opengis.net/gml/3.2.1/gml.xsd"
>
<wfs:Query typeNames='spatial_base:guangxi_cities'>
<wfs:PropertyName>geom</wfs:PropertyName>
<wfs:PropertyName>name</wfs:PropertyName>
<fes:Filter>
<fes:PropertyIsEqualTo matchAction="OR">
<fes:ValueReference>name</fes:ValueReference>
<fes:Literal>梧州市</fes:Literal>
</fes:PropertyIsEqualTo>
</fes:Filter>
</wfs:Query>
</wfs:GetFeature>
WFS 1.0.0 和 1.1.0 的又略有不同,詳見:
WFS 參考資料之複雜,中文教學、例子之少,其實講究效率的年代不願意用它也是情有可原的。
官方的例子,在標準檔案的 Annex B
章節(附件B)中。
線上的檔案,則可以參考 OGC Standard Examples 頁面,找到 WFS 目錄即可。
WFS 的事務允許你向伺服器上的資料進行增刪改,能增刪改的除了屬性資料,當然還包括圖形資料。
在 WFS 2.0.0 中,事務支援如下幾個動作(Action):
以更新為例,仍然是請求 4.3 小節中的地址,傳送的 XML 請求體則是:
<?xml version='1.0' encoding='UTF-8'?>
<wfs:Transaction
version="2.0.0"
service="WFS"
xmlns:fes="http://www.opengis.net/fes/2.0"
xmlns:wfs="http://www.opengis.net/wfs/2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.opengis.net/wfs/2.0 http://schemas.opengis.net/wfs/2.0.0/wfs.xsd">
<!-->要 Update 的要素是 spatial_base:guangxi_cities<-->
<wfs:Update typeName="spatial_base:guangxi_cities">
<!-->更新名稱屬性為「梧州市_重新命名」<-->
<wfs:Property>
<wfs:ValueReference>name</wfs:ValueReference>
<wfs:Value>梧州市_重新命名</wfs:Value>
</wfs:Property>
<!-->使用 Filtering 只修改第4個要素<-->
<fes:Filter>
<fes:ResourceId rid="guangxi_cities.4"/>
</fes:Filter>
</wfs:Update>
</wfs:Transaction>
有時候你無法傳送 Transaction
請求,返回的錯誤是「XXX is readonly」,應該是 GeoServer 對編輯請求作了限制,你可以到管理頁面的「Security - Data」下,將「*.*.w」的規則(也就是寫規則)賦予合適的角色,即可得到編輯寫入的許可權。
更多例子請參考文末給的例子連結,或者直接查閱對應 WFS 版本的標準檔案中的 Examples 內容。
WFS 的要務並不是拿來顯示大量向量資料的,大量的向量圖形資料對網路傳輸、瀏覽器渲染的效能要求非常高,甚至瀏覽器超過 1000 個常規的 JavaScript 物件就難以把持 rAF 程式的流暢性(一是遍歷效能可能不足,二是瀏覽器可用作業系統記憶體可能不夠)。
所以,不推薦 WFS 用來全量顯示向量圖形資料。
針對「既需要顯示、又需要查詢」的需求,可將任務分解:
OpenLayers6 有幾個 WFS 的例子,其使用 JavaScript 函數拼接請求引數之複雜令人困擾,實在令人提不起用標準 WFS 的慾望。
而 CesiumJS 則直接不考慮這個 OGC 服務,讓使用者自己選擇向量圖形資料的載入與否(可請求向量圖形資料後使用 GeoJsonDataSource
來載入)。
你在 WMTS 能力檔案中,一定能看到這兩個東西。但是我覺得官方檔案廢話太多,索性把自己的理解寫了出來。
TileMatrixSet
筆者譯為「瓦片陣集」,而 TileMatrix
即「瓦片陣」。
很多人也許第一次看到這個詞,一時半會兒想不通為什麼是「Matrix」。直譯來說,「Matrix」即矩陣:
假如這個矩陣的每個元素塊上填充的是地圖瓦片,那就能理解了。
所以,TileMatrix
指的就是某個層級的所有瓦片;自然而然,TileMatrixSet
就是所有層級的 TileMatrix
「集」。
舉例,GeoServer 中的內建瓦片陣集有一個是 EPSG:4326
,那麼第 7 級瓦片陣即 EPSG:4326:7
。
在 GeoServer 中還有個類似的詞是
Gridset
,在 TileCaching - Gridsets 下可以找到。
Z-order
,有時候又叫 莫頓曲線
,參考 wiki - Z-order 或 wikigis - Z-order。
在微軟 Bing 地圖瓦片的編號中使用了這個曲線。這個曲線通常用於四元樹的編碼。
瓦片的軸和原點均與 WMTS 一樣,只不過有一個語意上的等價關係:
如下圖所示:
OSM 和 谷歌地圖 的 Z、X、Y 也與 WMTS 一樣,是從 0 開始算的。
以 WMTS 的 TileMatrix=EPSG:900913:7
、TileCol=103
、TileRow=55
瓦片為例,那麼 OSM 的瓦片應為:
https://a.tile.openstreetmap.org/7/103/55.png
得到的瓦片:
而對應的谷歌地圖瓦片連結為:
http://mt2.google.com/vt/lyrs=m@167000000&hl=zh-CN&gl=cn&x=103&y=55&z=7
得到的圖:
再把原來 WMTS的瓦片搬來看看:
可以說位置上是一致的。
原點在 0 度經度、0 度緯度:
百度的 X 和 Y 值如上圖所示,有正有負。
瓦片層級,從 3 級起算,最大 21 級。
關於百度的 N 種座標,參考此文 百度地圖API詳解之地圖座標系統,業務上對高德、百度、騰訊等 LBS 廠商用得不多,故不再列舉,有需要的朋友可自行在網路上查詢。