0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

サーバーサイド・オーバーズーミングのベンチマーク

Posted at

サーバーサイド・オーバーズーミングについて、データの側面からのコンセプト実証のためのコードを書いてみて、実際に地理院地図 Vector タイルでベンチマークをとってみたので紹介します。

サーバーサイド・オーバーズーミングの必要性

ArcGIS API for JavaScript は、Mapbox GL JS や MapLibre GL JS 違い、クライアント側でオーバーズーミングをしないため、これを救済するためです。

oz-jp: サーバーサイド・オーバーズーミングのベンチマーク

constants.rb

要所を引用すると、次の通りです。

LIST_URL = 'https://optgeo.github.io/jp-tile-list/list.csv'
SRC_BASE_URL = 'https://cyberjapandata.gsi.go.jp/xyz/experimental_bvmap'

Z_LOT = 6
Z_SRC = 9
MINZOOM = 10
MAXZOOM = 18

Z_LOTは、一つのmbtilesファイルを作るタスクの空間的範囲を指定しています。ここでは、z=6のタイルの大きさを一つのタスクの空間的範囲にしています。日本全体を一度に変換してしまおうとすると、タスクが重くなりすぎるため、空間的範囲を定めて分割しています。

通常、私は上記の空間範囲をモジュールと呼ぶことが多いのですが、ここではロット(LOT)と呼んでいます。lotとは、a particular group, collection, or set of people or thingsという意味だそうです(New Oxford America Dictionary)。

Z_SRCは、オーバーズーミング元のzです。ここでは、z=9のベクトルタイルをオーバーズーミングしていることを指定しています。

MINZOOMは、オーバーズーミングで作るタイルの最小のzです。MAXZOOMは、オーバーズーミングで作る最大のzです。

ここでは、z=9のベクトルタイルを引き伸ばして、z=10..18のベクトルタイルを生産する、ということをするという定数の設定をしています。

Rakefile

require './constants'

desc 'produce tiles'
task :tiles do
  system "rm #{DST_DIR}/*.mbtiles" unless CONTINUE 
  sh <<-EOS
rm #{FIFO_DIR}/*; rm #{LOT_DIR}/*; \
curl #{LIST_URL} | grep ^#{Z_LOT}, | shuf | \
parallel --jobs=1 --line-buffer \
ruby produce_lot.rb {}; 
tile-join -o #{MERGED_DIR}/#{Z_SRC}.mbtiles #{LOT_DIR}/*.mbtiles
  EOS
end

curl #{LIST_URL} | grep ^#{Z_LOT}, | shuf |により、生産するべきロットのタイル番号を引き出して渡しています。shufによってシャッフルするのは、日本の場合特に西側は島嶼地域であり、変換の速度感を把握しにくくなるため、なるべく隅に偏ることなくタスクを試すことが有効であるからです。

parallel --jobs=1 --line-buffer ruby produce_lot.rb {} が変換の本体です。ロットのタイル番号を引数として与えながら、ロットごとにオーバーズーミングをバッチ処理していきます。

produce_lot.rb

メイン処理に当たるところは、次の通りです。

dst_path = "#{DST_DIR}/#{LOT_ZXY.join('-')}.mbtiles"
if File.exist?(dst_path)
else
  hooks = charge
  jump_into(LOT_ZXY)
  withdraw(hooks)

  system <<-EOS
tile-join -o #{dst_path} \
# {LOT_DIR}/#{LOT_ZXY.join('-')}*.mbtiles
  EOS
end

つまり、コマンドライン引数で渡されたタイル番号から mbtiles のパスを作り、そこにファイルが存在しなければ、charge をして jump_into をして withdraw をし、最後に tile-join でマージをする、という流れです。

charge

これは、レイヤの数だけ Tippecanoe プロセスを用意するものです。Tippecanoe の標準入力につながった名前つきパイプを用意しています。ベクトルタイルはレイヤごとに分かれているのですが、今回の処理で使う vt2geojson がレイヤを指定することでしかレイヤを取り出せないようになっているので、このようにしています。

def charge
  hooks = Hash.new
  LAYERS[Z_SRC].each {|layer|
    fifo_path = "#{FIFO_DIR}/#{LOT_ZXY.join('-')}-#{layer}"
    mbtiles_path = "#{LOT_DIR}/#{LOT_ZXY.join('-')}-#{layer}.mbtiles"
    system "rm #{fifo_path}" if File.exist?(fifo_path)
    system "mkfifo #{fifo_path}"
    hooks[layer] = {
      :fifo => File.open(fifo_path, 'w+'),
      :pid => spawn(
        <<-EOS
tippecanoe --force --layer=#{layer} -o #{mbtiles_path} \
--minimum-zoom=#{MINZOOM} --maximum-zoom=#{MAXZOOM} \
--detect-shared-borders --coalesce --hilbert \
< #{fifo_path} 2>&1
        EOS
      )
    }
  }
  hooks
end

jump_into

引数として渡されるタイル番号を解釈し、それが読み込むべきzであればprocessを呼び出し、そうでなければ、そのタイル番号に包含される4つのタイルについてjump_intoを呼び出します。これは典型的な再帰的処理だ、と私は思います。

def jump_into(zxy)
  if zxy[0] == Z_SRC
    process(zxy)
  else
    2.times {|i|
      2.times {|j|
        jump_into([
          zxy[0] + 1,
          2 * zxy[1] + i,
          2 * zxy[2] + j
        ])
      }
    }
  end
end

withdrawを見る前に、このjump_intoから呼び出されるprocessを見てみます。

process

読み込むべきタイル番号が与えられて呼び出される関数です。タイル番号から  URL を作り、実際にタイルをダウンロードして、正常に読み込まれれば、(go言語で書かれた方の)vt2geojsonでGeoJSONに展開して、名前つきパイプ経由でTippecanoeにフィードします。

def process(zxy)
  print "process #{zxy} in #{LOT_ZXY}\n"
  tile_path = "#{TMP_DIR}/#{zxy.join('-')}.pbf"
  status = `curl #{SRC_BASE_URL}/#{zxy.join('/')}.pbf -o #{tile_path} -w '%{http_code}' -s`
  if status == '200'
    LAYERS[Z_SRC].each {|layer|
      fifo_path = "#{FIFO_DIR}/#{LOT_ZXY.join('-')}-#{layer}"
      system <<-EOS
# {VT2GEOJSON_PATH} -gzipped=false -mvt #{tile_path} -layer #{layer} \
-z #{zxy[0]} -x #{zxy[1]} -y #{zxy[2]} \
| tippecanoe-json-tool > #{fifo_path}
      EOS
    }
  end
  system "rm #{tile_path}"
end

withdraw

処理するべき全てのタイルについて process された後で呼び出される関数です。名前つきパイプを閉じることで、Tippecanoeによるタイル生産を起動させ、Tippecanoeの終了を待ってから名前付きパイプを削除します。

def withdraw(hooks)
  LAYERS[Z_SRC].each {|layer|
    fifo_path = "#{FIFO_DIR}/#{LOT_ZXY.join('-')}-#{layer}"
    print "Producing vector tiles for #{fifo_path} (pid #{hooks[layer][:pid]})...\n"
    $stdout.flush
    hooks[layer][:fifo].flush
    hooks[layer][:fifo].close
    Process.waitpid(hooks[layer][:pid])
    system "rm #{fifo_path}"
    print "done (#{fifo_path}, pid #{hooks[layer][:pid]}).\n"
  }
end

計測値

実際に変換をしてみて、えられた mbtiles の内部のサイズ分布を vt-optimizer で分析した結果を共有します。この作業での私による有効数字の扱いはいい加減で、vt-optimizerの表示を MB や GB に変換する際には、1MB=1000kB, 1GB=1000000kBというようなざっくりした読み替えをしています。

また、実際の変換は、USB HDDを接続した Raspberry Pi 4B で実施しています。

 z=12を用いたz=16までのオーバーズーミング

z  タイル数 総容量 平均容量 最大容量
13 9万 95MB 1KB 12KB
14 36万 149MB 0.4KB 7KB
15 139万 294MB 0.2KB 4KB
16 542万 747MB 0.1KB 3KB

z=10を用いたz=16までのオーバーズーミング

z  タイル数 総容量 平均容量 最大容量
11 1.7万 18MB 1KB 34KB
12 6.4万 25MB 0.4KB 13KB
13 25万 46MB 0.2KB 5KB
14 99万 115MB 0.1KB 4KB
15 392万 358MB 0.1KB 3KB
16 1531万 1.3GB 0.1KB 2KB

z=9を用いたz=18までのオーバーズーミング

z  タイル数 総容量 平均容量 最大容量
10 4千 8MB 1.9KB 45KB
11 1.7万 10MB 0.6KB 19KB
12 6.4万 17MB 0.3KB 7KB
13 25万 35MB 0.1KB 3KB
14 99万 99MB 0.1KB 3KB
15 390万 330MB 0.09KB 1KB
16 1500万 1.2GB 0.08KB 1KB
17 5900万 3.5GB 0.08KB 0.7KB
18 2.3億 18GB 0.08KB 0.5KB

ズームレベル18までの生産をすると、上記のとおり膨大なストレージを使うことになります。mbtiles の集約にも、実時間で50時間以上を使ってしまいました。工夫の余地はあると思いますが、工夫をしなければ現実的なソリューションを得ることが難しそうです。

z=15を用いたz=18までのオーバーズーミング (in progress)

z  タイル数 総容量 平均容量 最大容量
16 * * * *
17 * * * *
18 * * * *

現時点での感想

必要に迫られてやむなく1, 2ズームレベル分のオーバーズーミングをすることは否定しないとはいえ、ArcGIS API for JavaScript救済が唯一の目的である本件に、かけられるコストの余裕はそれほどないように感じています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?