1. はじめに
この記事は、MapLibre GL JS を使った地図スタイルに関して、少し細かいところのネタです。
MapLibre GL JSのスタイルjson(あるいはタイルjson)で、sourcesのなかのtilesのURLを複数併記するとどうなるか調べてみました。うまく利用すると、ソースのURLを複数指定することで(ソースの下の要素を複数にするということでなく)、少し大きなソースデータでも複数の参照先から読み込むことができました。下の3つのタイルのように複数のソースURLでも、一つのソースとして扱うことができます。(ただし、MapLibreのスタイル仕様上に明記されている仕組みではありません。)
いままで1GBを超えるベクトルタイルのデータセットはGitHubページでホストしていませんでしたが、複数のGitHubページをつかって数GBのデータをホストさせる方法ができるようになると思います。
この実験の動機
先日、タイルのソースURLを2つ指定した場合に、それぞれのタイルの参照先がきれいなごま塩状(市松文様、チェック状)に分布することを報告しました(こちらの記事です)。
その時は深く考えなかったのですが、縮尺を変えても縞模様になることがなく、きれいなごま塩状のままなので、そこにはやはり規則があるのかもしれないと思い探ってみました。
2. 観察
2-1. タイルのソースURLがが2つの場合
まず、2つのURLの場合を観察します。いくつかのズームレベルでタイルの出現パターンを確認します。具体的には https://ubukawa.github.io/vector-tile-nj のページでZL5から8まで確認しました。この地図の参照しているスタイル(スタイルjsonの先のタイルjson)では、ソースのタイルURLを2つ指定しています(NJ
とNY)。
ズームレベル8 NYとNJは市松文様のように入り乱れています。
2-2. 2つのタイルソースを使った例から考えた仮説
観察した範囲では、どのズームレベルでもタイルの参照先は、タイル番号のxとyの偶奇で決まっているような印象を受けました。
<仮説>
スタイルファイルのソースで、タイルのURLを複数指定した場合、参照先URLは順番に指定される。特に、タイルURLが二つ指定されている場合:
- タイル番号z/x/yの(x,y)が(偶数, 偶数)または(奇数,奇数)のときは、最初に指定したURL(tiles[0])が参照される。
- タイル番号z/x/yの(x,y)が(偶数, 奇数)または(奇数,偶数)のときは、2番目に指定したURL(tiles[1])が参照される。
2-3. タイルのソースURLがが3つの場合
さらに検討するために、せっかくなので、ソースのURLを3つにしてました。3つ目がなかったので、2番目と3番目は同じものにしますが、これで傾向を観察します。
下のような感じでした。ここはNJ州の地区なので、空白がNY、地図ありがNJです。NJ-NY-NYとリズミカルに出現しています。
2-4. ソースの数が3の場合も含めた仮説
ソースが三つの時はX方向に、tiles[0]、tiles[1]、tiles[2]と並んで、Y方向にはtiles[0]、tiles[2]、tiles[1]と並んでいくような模様になるのかもしれませんね。もっと想像すると、 x + y をソースの数で割った余りによって、読み込む先を決めているような印象があります。ソース2つの時と3つの時の雰囲気を踏まえて仮説をもう少し一般化します。
- 2つのソースのとき、x+yが0 (mod 2)ならtiles[0]を参照し、x+yが1(mod 2)ならtiles[1]を参照していると仮説を立てました。
- 3つのソースのとき、x+yが0 (mod 3)ならtiles[0]を参照し、x+yが1(mod 3)ならtiles[1]を参照し、x+yが2(mod 3)ならtiles[2]を参照していると仮説を立てました。
- nつのソースのとき、x+y ≡ m (mod n)なら、tiles[m]を参照する。
これは3つ以上のソースの時にも拡張できるのではないだろうか・・・。
3. 仮説を試す
参照先についてある仮説が立てられました。これが正しくて、そしてうまく利用できれば、タイルソースを二つ(やそれ以上)のGitHubレポジトリ(GitHubページ)に分けてホストしても、一つのデータソースとして扱える可能性があります。
今回は、ラスタータイルですが、標高RGBタイルを分けてホストする実験をしてみます。これまで データサイズの関係で、ZL7までしかアップできなかったグローバルな標高タイル( https://github.com/ubukawa/globalmap-el )があるので、これを3つのレポジトリに分けてZL8までアップロードしてみます。データサイズはZL2-8で2.1GBくらいでしたので、3つに分けてみることにしました。
3-1. タイルデータをサブディレクトリにコピーするスクリプトを書く
タイルデータを3つのサブフォルダにコピーしていきます。3つにわけるので、タイル番号のx+yが0 (mod 3)ならtiles[0]で指定するためサブグループ1、x+yが1(mod 3)ならサブグループ2、x+yが2(mod 3)ならサブグループ3になるようにコピーします。
手作業でコピーするのは大変なので、nodejsで処理します。本当はbashかshかで処理したいのですが、うまくできなさそうだったので、nodejsのfsモジュールなどを使って作業しました。
作ったレポジトリはこちら: https://github.com/ubukawa/divider
3つに分けたいので、configのファイルではdivNumを3にしてあります。
{
minzoom: 2
maxzoom: 8
srcDir: src/zxy
outDir: out
extension: png
divNum: 3
}
//modules
const config = require('config')
const fs = require('fs')
//parameters
const minzoom = config.get('minzoom')
const maxzoom = config.get('maxzoom')
const srcDir = config.get('srcDir')
const extension = config.get('extension')
const outDir = config.get('outDir')
const divNum = config.get('divNum')
//make division
for (let i=1; i < divNum + 1; i++){
fs.mkdirSync(`${outDir}/zxy_${i}_${divNum}`, {recursive: true},
(err) =>{
if(err){throw err}
})
}
//for each z/x/y
for (var z = minzoom; z < maxzoom + 1 ; z++){
console.log(`Zoom level ${z} started at ${Date()}`)
for (let x = 0; x < 2 ** z; x++){
for (let y = 0; y < 2 ** z; y++){
if(fs.existsSync(`${srcDir}/${z}/${x}/${y}.${extension}`)){
let modXY = (x + y) % divNum
let division = modXY + 1
let filePath = `${outDir}/zxy_${division}_${divNum}`
if(!fs.existsSync(`${filePath}/${z}`)){
fs.mkdirSync(`${filePath}/${z}`)
console.log(`${filePath}/${z} has been created.`)
}
if(!fs.existsSync(`${filePath}/${z}/${x}`)){
fs.mkdirSync(`${filePath}/${z}/${x}`)
console.log(`${filePath}/${z}/${x} has been created.`)
}
//console.log(`${z}/${x}/${y}.${extension} exists. Its X+Y value is ${modXY} (mod: ${divNum}), so it will go to ${filePath} `)
fs.copyFileSync(`${srcDir}/${z}/${x}/${y}.${extension}`,`${filePath}/${z}/${x}/${y}.${extension}`)
console.log(`${z}/${x}/${y}.${extension} exists. Its X+Y value is ${modXY} (mod: ${divNum}), so it has been copied in ${filePath}`)
}
//console.log(`no file: ${z}/${x}/${y}` )
}
}
}
3-2. プログラムを実行してサブグループに分ける
書いたものを実行します。
git clone https://github.com/ubukawa/divider
cd divider
npm install
vi config/default.hjson # 適宜設定を調整する。もとになるデータの場所なども指定する。
node index.js
3つのフォルダに分かれてコピーできました。それぞれ700MBと少しです。
思ったようにコピーできています。
3-3. GitHubレポジトリを作ってそれぞれのタイルをアップロード
GitHubレポジトリを3つ作ってデータをアップロードしました。GitHubページの設定をしたので、タイルは以下のURLでアクセスできるようになりました。
- "https://ubukawa.github.io/globalmap-el-sub1/zxy_1_3/{z}/{x}/{y}.png"
- "https://ubukawa.github.io/globalmap-el-sub2/zxy_2_3/{z}/{x}/{y}.png"
- "https://ubukawa.github.io/globalmap-el-sub3/zxy_3_3/{z}/{x}/{y}.png"
3-4. maplibre gl js を使った地図とスタイルの作成
地図とスタイルを以下の通り作ります。
https://github.com/ubukawa/globalmap-el-sub1/blob/main/map.html
https://github.com/ubukawa/globalmap-el-sub1/blob/main/test.json
スタイルでラスタタイルのURLを複数指定しますが、その順番には気を付けます。作ったサブグループの順番に並ぶようにします。
3-5. 成果のチェック
標高タイルが空白の部分がなくうまく読み込めていることがわかりました。少なくとも今回のテストの範囲では仮説はあっていたのだと思います。
個別のレポジトリのタイルは以下のような感じです(ZL4のみ)。
-
サブグループ1: https://ubukawa.github.io/globalmap-el-sub1/single-source.html#4.08/38.25/73.92
-
サブグループ2: https://ubukawa.github.io/globalmap-el-sub2/single-source.html#4.08/38.25/73.92
-
サブグループ3: https://ubukawa.github.io/globalmap-el-sub3/single-source.html#4.08/38.25/73.92
まとめと考察
複数の参照先URLにあるタイルデータを一つのソースとして読むことができることを確認しました。具体的には、これまでデータサイズの関係でZL2-7までしかアップロードできなかった標高タイルについて、ZL8までを3つのレポジトリにアップロードして参照することができました。
- sourcesのtilesにURLを複数書いたときの、MapLibre GL JSの反応をある程度理解できました。それぞれのURLで参照されるタイルの分布が面白いと思います。MapLibreを開発した人にとっては当たり前のことなのかもしれませんが、私にとっては新しい発見です。
- sourcesの直下の要素を増やすとスタイルレイヤもそれも応じて増やす必要がありますが、sources中の一つのsourceのtiles参照先を増やすことで複数のURLを一つのソースとして扱うことが出来ました。
- この反応はMapbox GL JS、Esri のArcGIS API for Javascripts、QGISのプラグイン等でどう反応するかは試せていません。MapLibre GL JSにおいても、スタイル仕様に明記されたルールではないので、予告なく変更されるリスクもある点には留意が必要です。
- 今回は1GBを超えるデータをGitHubページでホストするという応用事例を試しましたが、ホスティングサーバーを複数立てての負荷を分散させたいときなどにも応用できるかもしれません。
- 同じようにarrayで参照するものを併記するのに、text-fontがあります。text-fontはスタイル仕様上で複数の併記が認められていますが、これまで私は成功したことがなかったです。テキストフォントの併記とソースURLの併記で仕組みがどう違うのかも今後考えていきたいと思います。
- (今回の標高タイルについては、もしかしたら海の部分を削除したらもっと全体サイズを小さく出来るかも・・・、と気づきました。)
ということで、かなり細かいネタですが、ここにメモしておきます。
参考
- MapLibre style spec (vetor tile - source - tiles) : https://maplibre.org/maplibre-gl-js-docs/style-spec/sources/#vector-tiles
- MapLibre style spec (raster tile - source - tiles) : https://maplibre.org/maplibre-gl-js-docs/style-spec/sources/#raster-tiles
- Mapbox style spec (vetor tile - source - tiles) : https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#vector-tiles
- Mapbox style spec (raster tile - source - tiles) : https://docs.mapbox.com/mapbox-gl-js/style-spec/sources/#raster-tiles
- GitHub repositories:
- Global elevation tile from Global Map (ZL2-7): https://github.com/ubukawa/globalmap-el
- Global Map (ZL2-8) - sub group 1: https://github.com/ubukawa/globalmap-el-sub1
- Global Map (ZL2-8) - sub group 2: https://github.com/ubukawa/globalmap-el-sub2
- Global Map (ZL2-8) - sub group 3: https://github.com/ubukawa/globalmap-el-sub3
- My repository for making sub groups: https://github.com/ubukawa/divider
- Test maps
- https://ubukawa.github.io/globalmap-el-sub1/map.html
- map with only subgroup 1 (ZL4 tile only): https://ubukawa.github.io/globalmap-el-sub1/single-source.html
- map with only subgroup 2 (ZL4 tile only): https://ubukawa.github.io/globalmap-el-sub2/single-source.html
- map with only subgroup 3 (ZL4 tile only): https://ubukawa.github.io/globalmap-el-sub3/single-source.html