TMS のタイルデータソースがあり、「模倣」して WMTS サービスを作成する必要があります。どうすればよいですか?
この状況では、実際には既存のインフラストラクチャやツールを使用して解決できます。例えば、さまざまな地図サーバーや、.net エコシステムには tile-map-service-net5のようなオープンソースツールがあります。この問題が問題である理由は、2 つの制約条件にあります。
- 使用するクライアントが XYZ/TMS 形式のデータを読み込むことをサポートしておらず、WMS および WMTS 形式のデータのみを読み込むことができます。
- 使用するデータは、切り分けられた TMS 構造のデータです。
- クライアントが外部地図サーバーに依存するのが難しいです。
模倣リソースリンク#
私たちがよく知っているインターネット地図は、XYZ または TMS の方式を使用しています。例えば、OSM、Google Map、Mapbox などです。以前のラスタタイルから現在のベクタタイルがより一般的になっています。TMS を使用して WMTS のリクエスト形式を「模倣」するには、まず彼らの違いを理解する必要があります。
XYZ(スリッピーマップタイル名)#
- 256*256 ピクセルの画像
- 各ズームレベルは 1 つのフォルダーで、各カラムはサブフォルダーで、各タイルは行で命名された画像ファイルです。
- フォーマットは
/zoom/x/y.png
のようになります。 - x は (
180°W ~ 180°E
)、y は(85.0511°N ~85.0551°S
)、Y 軸は上から下に向かっています。
Openlayers TileDebug Exampleから、シンプルな XYZ タイルの例を見ることができます。
TMS#
TMS の Wiki wikipediaには詳細が含まれておらず、osgeo-specificationではプロトコルのいくつかのアプリケーションの詳細が説明されています。逆に、geoserver docsの TMS に関する部分はより実用的に書かれています。TMS は WMTS の前身であり、OSGeo が策定した標準です。
リクエストは次のようになります:
http://host-name/tms/1.0.0/layer-name/0/0/0.png
さまざまなファイル形式や空間参照システムをサポートするために、複数のパラメータを指定することもできます:
http://host-name/tms/1.0.0/layer-name@griset-id@format-extension/z/x/y
TMS 標準のタイルグリッドは左下隅から始まり、Y 軸は下から上に向かいます。一部の地図サーバー、例えば geoserver は、Y 座標を反転させるための追加のパラメータflipY=true
をサポートしており、これにより、WMTS や XYZ のように Y 軸が上から下に向かうサービスタイプと互換性を持たせることができます。
WMTS#
WMTS は、上記の 2 つの直感的なプロトコルに比べて、内容がより複雑で、サポートされるシナリオも多くなっています。2010 年にOGCによって初めて発表されました。この前、1997 年に Allan Doyle の論文「Www mapping framework」の後、OGC はネットワーク地図に関連する標準の策定を開始しました。WMTS の前、最も古く、最も広く使用されているネットワーク地図サービス標準はWMSです。WMS は、各リクエストがユーザーの地図のズームレベルと画面サイズに基づいて地図の応答を構成するため、これらの応答のサイズはさまざまであり、マルチコア CPU が普及していなかった当時、このオンデマンドでリアルタイムに生成される地図の方法は非常に贅沢でした。同時に、応答速度を向上させることは非常に困難でした。そこで、開発者はタイルを事前に生成する方法を試み始め、多くのソリューションが生まれました。前述の TMS はその一つであり、その後 WMTS が登場し、広く使用されるようになりました。WMTS は、キーと値のペア(kvp)および Restful 方式でリクエストパラメータをエンコードします。
KVP は次のようになります:
<baseUrl>/layer=<full layer name>&style={style}&tilematrixset={TileMatrixSet}}&Service=WMTS&Request=GetTile&Version=1.0.0&Format=<imageFormat>&TileMatrix={TileMatrix}&TileCol={TileCol}&TileRow={TileRow}
Restful は次のようになります:
<baseUrl>/<full layer name>/{style}/{TileMatrixSet}/{TileMatrix}/{TileRow}/{TileCol}?format=<imageFormat>
これはラスタタイルであるため、ここでは XYZ とタイルマトリックスおよびタイル行列番号の対応関係を見つけるだけで済みます。
- TileMatrix
- TileRow
- TileCol
ここでのタイル行列番号は左上隅から始まり、Y 軸は上から下に向かいます。
これにより、TMS と WMTS の各パラメータの対応関係が見つかりました。次は、TMS を WMTS のリクエストに変換する方法です。以下のようになります:
- TileRow = 2^zoom - 1 - y = (1 << zoom) - 1 - y
- TileCol = x
- TileMatrix = zoom
他の空間参照を考慮しない場合、ズームレベルはタイルマトリックスに対応し、x はタイル列番号に対応し、y は反転します(初期方向が逆のため)。
WMTS Capabilities の模擬記述ファイル#
WMTS 仕様の要求は、ほぼ髪の毛のように細かいので、各クライアントは、Web の Openlayers やデスクトップの QGIS や Skyline など、Capabilities 記述ファイルを直接解析し、その内容に基づいてレイヤー、スタイル、空間参照を選択することをサポートしています。したがって、ここでも WMTS Capabilities 記述ファイルを模擬する必要があります。
Capabilities 記述ファイルの構成#
WMTS Capabilities 記述ファイルの例は、opengis schemaや、天地图山东で見つけることができます。
Capabilities 記述ファイルの内容は非常に多く、ここではいくつかの重要な部分(タイトル、連絡先などは無視)を示します:
OperationsMetadata:
- GetCapabilities >> Capabilities記述ファイルの取得方法
- GetTile >> タイルの取得方法
Contents:
- Layer
- boundingBox >> レイヤーの経緯度範囲
- Style
- TileMatrixSetLink >> レイヤーがサポートする空間参照
- TileMatrixSet >> 空間参照
- TileMatrixSetLimits >> 空間参照のズームレベル範囲
- TileMatrixLimits >> 各ズームレベルのタイル行列番号範囲
- Style
- TileMatrixSet
- TileMatrix
重要な部分は boundingBox、TileMatrixSetLimits、TileMatrixLimits です。これらはレイヤーの空間参照とズームレベルに基づいて計算する必要があります。
boundingBox の計算は比較的簡単で、レイヤーの経緯度範囲ですので、ここでは詳しく説明しません。
TileMatrixSetLimits の計算は比較的簡単で、レイヤーの空間参照のズームレベル範囲です。
TileMatrixLimits の計算は比較的複雑で、レイヤーの範囲が比較的小さい場合にのみ行うことができます。世界地図の場合は必要ありません。これはレイヤーの空間参照とズームレベルに基づいて計算する必要があります。以下は擬似コードの一部です(4326 から 3857 への変換)。
FUNCTION GetTileRange(minLon, maxLon, minLat, maxLat, zoom, tile_size = 256)
minLonRad = minLon * PI / 180
maxLonRad = maxLon * PI / 180
minLatRad = minLat * PI / 180
maxLatRad = maxLat * PI / 180
tile_min_x = Floor((minLonRad + PI) / (2 * PI) * Pow(2, zoom))
tile_max_x = Floor((maxLonRad + PI) / (2 * PI) * Pow(2, zoom))
tile_min_y = Floor((PI - Log(Tan(minLatRad) + 1 / Cos(minLatRad))) / (2 * PI) * Pow(2, zoom))
tile_max_y = Floor((PI - Log(Tan(maxLatRad) + 1 / Cos(maxLatRad))) / (2 * PI) * Pow(2, zoom))
// タイルサイズに基づいてタイル範囲を調整
tile_min_x = Floor((double)tile_min_x * tile_size / 256)
tile_max_x = Ceiling((double)tile_max_x * tile_size / 256)
tile_min_y = Floor((double)tile_min_y * tile_size / 256)
tile_max_y = Ceiling((double)tile_max_y * tile_size / 256)
RETURN (tile_min_x, tile_max_x, tile_min_y, tile_max_y)
WMTS Capabilities 記述ファイルの生成#
最小限の WMTS Capabilities 記述ファイルを生成し、上記の重要な部分を埋め込み、その後、標準記述ファイルのアドレスを指す Restful スタイルの URL を構築します。
後記#
以上は TMS から WMTS へのシンプルな考え方ですが、実際には空間参照の変換、ズームレベルの変換、タイル行列番号の変換、タイルのフォーマット変換など、考慮すべき多くの詳細があります。
その間にいくつかの落とし穴にもはまり、この部分がより面白いと感じました。
第一部では、すぐにtile-map-service-net5の考えを参考にして、y >> tileRow
の変換を完了しました。コードはWebMercator.csにあります。実際、StackOverflow でも誰かがこの問題を尋ねており、答えがありましたが、私はソフトウェアの中で答えを見つけることを選びました。そうすれば、自分の心の中でより安心できるからです。
第二部は非常に頭が痛く、まずリソースリンクを模倣し、シンプルな XML を構築しましたが、ターゲットクライアントで直接読み込むことができませんでした。非常に直接的に標準サービスをテストすることを考え、その後 Capabilities 記述ファイルを修正することにしました。最初は比較的慣れている Openlayers でテストし、その後 Capabilities 記述ファイルを修正することにしました。Openlayers の読み込み方法は非常に柔軟で、Capabilities 記述ファイルがない場合でも、パラメータを設定することで直接アクセスできます。
// WMTS Capabilitiesを取得して解析する
const options = optionsFromCapabilities(capabilities, {
layer: 'nurc:Pk50095',
matrixSet: 'EPSG:900913',
format: 'image/png',
style: 'default',
});
const wmts_layer = new TileLayer({
opacity: 1,
source: new WMTS(options),
})
残念ながら、タイルは読み込まれず、networks
にはリクエストが送信されていませんでした。そこで、別の WMTS 関連の例に行き、カスタム TileGrid を定義し、タイルの行列番号を 3857 の行列番号に変換しました。この時点で、タイルが読み込まれるようになりました。
const projection = getProjection('EPSG:3857');
const projectionExtent = projection.getExtent();
const size = getWidth(projectionExtent) / 256;
const resolutions = new Array(31);
const matrixIds = new Array(31);
for (let z = 0; z < 31; ++z) {
// このWMTSのために解像度とmatrixIds配列を生成
resolutions[z] = size / Math.pow(2, z);
matrixIds[z] = `EPSG:900913:${z}`;
}
var wmtsTileGrid = new WMTSTileGrid({
origin: getTopLeft(projectionExtent), resolutions: resolutions, matrixIds: matrixIds,
})
TileGrid の問題を確認した後、まず自分が生成した TileGrid と Openlayers が Capabilities から解析した TileGrid を比較しました。自分が生成した TileGrid にはいくつかのフィールドが空であることがわかりましたので、1 つずつテストし、最終的にfullTileRanges_
とextent_
の 2 つの内部パラメータが空である場合、画像が読み込まれることがわかりました。
OL のソースコードを調べると、fullTileRanges_
とextent_
はgetFullTileRangeで使用されていることがわかりました。
つまり、fullTileRanges_
とextent_
が空である場合、getFullTileRange
は空の範囲を返します。
また、getFullTileRange
はwithinExtentAndZで使用されており、ここでは現在の可視領域にそのレイヤーのタイルがあるかどうかを判断するために使用されます。つまり、fullTileRanges_
とextent_
が空である場合、TileRange
を取得できず、withinExtentAndZ
は常にtrue
を返し、タイルが常に読み込まれることになります。これが読み込み成功の理由です。
逆に、Capabilities から解析されたfullTileRanges_
とextent_
は誤ったTileRange
を指しており、withinExtentAndZ
は常にfalse
を返すため、タイルが読み込まれないことになります。これが読み込み失敗の理由です。
ついに原因を見つけましたが、ここでまた騙されました。wmts.jsのコンストラクタには次のようなコメントがあります:
class WMTS extends TileImage {
/**
* @param {Options} options WMTSオプション。
*/
constructor(options) {
// TODO: TileMatrixLimitsのサポートを追加
}
}
これにより、最初はfullTileRanges_
とextent_
が経緯度範囲(boundingBox)に基づいて計算されると思い込み、TileMatrixLimits
に基づいて計算されるのではないかと誤解しました。そこで、boundingBox を再確認し、問題がないことを確認した後、TileMatrixLimits
の修正に取り掛かりました。
最初は TileMatrixLimits が各レベルのタイル範囲であると思っていたため、レイヤーの範囲ではないことに気づかず、このパラメータに注意を払わなかったため、遠回りをしてしまいました。
2023 年に書いていますが、WMTS はもはや新しいプロトコルではなく、OGC Tile API は正式な標準となっています。自分の WMTS に対する理解はまだ半分しかないと感じ、恥ずかしい限りです😅。