コンテンツにスキップ

第06回 ベクトルタイル

科目: ビッグデータ応用Ⅱ〔特徴量エンジニアリング〕
担当: 小野原彩香
対象: 3年生(後期)


学習目標

  1. ベクトルタイルの概念(XYZ座標、ズームレベル、MVT形式)を説明できる
  2. Tippecanoe を使ってGeoJSONをpmtilesまたはmbtiles形式に変換できる
  3. 属性の保持・削除などの最適化オプションを使いこなせる
  4. MapLibre GL JSを使ってブラウザでベクトルタイルを表示するHTMLを作成できる

1. ベクトルタイルとは

1.1 地図タイルの基礎

Webマップでは、地図データを小さな正方形(タイル)に分割して配信します。ユーザーが見ている範囲のタイルだけを読み込むため、大量のデータでも高速に表示できます。

タイルは Z/X/Y の3つの数値で識別されます。

パラメータ 意味
Z(ズームレベル) 縮尺。0が世界全体、22が最大ズーム
X 横方向のタイル番号(西から東)
Y 縦方向のタイル番号(北から南)

1.2 ラスタータイルとベクトルタイルの違い

比較項目 ラスタータイル(PNG/JPG) ベクトルタイル(MVT)
データ形式 画像ピクセル ベクトル形状 + 属性
スタイル変更 再生成が必要 クライアント側で動的変更可
ファイルサイズ 大きい 小さい
拡大したときの見え方 ぼける シャープ
属性情報 保持できない 保持できる(クリックで取得可)
代表ツール GDAL, MapProxy Tippecanoe, PMTiles

1.3 MVT(Mapbox Vector Tiles)形式

ベクトルタイルの標準形式は MVT(Mapbox Vector Tiles)です。Protocol Buffersでエンコードされた .pbf ファイルで、タイルごとに地物のジオメトリと属性が格納されています。

1.4 配信形式:MBTilesとPMTiles

形式 概要
.mbtiles SQLiteベースのタイルコンテナ。サーバーが必要
.pmtiles 単一ファイル、HTTP Range Requestで直接配信可能

本講義では、シンプルにCDNや静的ホスティングから配信できる PMTiles を主に使用します。


2. Tippecanoeの概要とインストール

2.1 Tippecanoeとは

Tippecanoe はMapboxが開発したCLIツールで、GeoJSONなどのベクトルデータをMBTilesまたはPMTilesに変換します。主な特徴:

  • ズームレベルごとの自動間引き(地物の簡略化)
  • 属性の選択的保持・削除
  • 大規模データ(数百万件)の高速処理

2.2 インストール

# macOS(Homebrew)
brew install tippecanoe

# Ubuntu / Debian
sudo apt-get install -y tippecanoe

# バージョン確認
tippecanoe --version
# 出力例: tippecanoe v2.xx.x

2.3 関連ツール

# tile-join: 複数の mbtiles/pmtiles を結合
# tile-decode: タイルの内容を確認

# インストール確認
which tile-join
which tile-decode

3. 基本的なTippecanoeコマンド

3.1 最もシンプルな変換

# GeoJSON → PMTiles(最もシンプル)
tippecanoe \
  -o output.pmtiles \
  input.geojson

# -o : 出力ファイル
# ズームレベルは自動決定(データの密度に応じる)

3.2 ズームレベルの指定

# ズームレベルを明示的に指定
tippecanoe \
  -o output.pmtiles \
  -Z 8 \
  -z 14 \
  input.geojson

# -Z : 最小ズームレベル(広域表示)
# -z : 最大ズームレベル(詳細表示)
# 例: Z=8(県レベル)〜z=14(街区レベル)

3.3 LLMアクセスログデータへの適用例

# LLMログの空間結合済みGeoJSONをpmtilesに変換
tippecanoe \
  -o llm_access_tiles.pmtiles \
  -z 14 \
  -Z 8 \
  --layer=llm_access \
  llm_access_joined.geojson

# --layer : レイヤー名の指定(MapLibreで参照する名前)

3.4 MBTilesへの変換

# PMTilesの代わりにMBTilesを生成したい場合
tippecanoe \
  -o output.mbtiles \
  -z 14 \
  -Z 8 \
  input.geojson

4. タイルの最適化オプション

4.1 属性の保持と削除

大量の属性列がある場合、不要な属性を削除することでタイルサイズを大幅に削減できます。

# 特定の属性のみ保持(--include)
tippecanoe \
  -o output.pmtiles \
  -z 14 -Z 8 \
  --include=ward_name \
  --include=access_count \
  --include=area_km2 \
  llm_access_joined.geojson

# 特定の属性を削除(--exclude)
tippecanoe \
  -o output.pmtiles \
  -z 14 -Z 8 \
  --exclude=geometry_source \
  --exclude=internal_id \
  llm_access_joined.geojson

# 全属性を削除してジオメトリのみ保持
tippecanoe \
  -o output.pmtiles \
  -z 14 -Z 8 \
  --exclude-all \
  llm_access_joined.geojson

4.2 地物の間引きオプション

# ズームレベルが低いとき地物を間引かない(すべて保持)
tippecanoe \
  -o output.pmtiles \
  -z 14 -Z 8 \
  --drop-densest-as-needed \
  llm_access_joined.geojson

# 間引きをせず全地物を全ズームで保持(ファイルサイズが大きくなる)
tippecanoe \
  -o output.pmtiles \
  -z 14 -Z 8 \
  -r 1 \
  llm_access_joined.geojson

# -r 1 : 間引き率を1(保持率100%)に設定

# ポイントデータの場合はクラスタリングが有効
tippecanoe \
  -o output.pmtiles \
  -z 14 -Z 8 \
  --cluster-distance=10 \
  llm_access_points.geojson

# --cluster-distance : 指定ピクセル内のポイントをクラスタ化

4.3 ファイルサイズの確認

# 変換後のファイルサイズ確認
ls -lh output.pmtiles

# タイルの内容を確認(tile-decode)
tile-decode output.pmtiles 10/906/403

# PMTilesの統計情報を確認
pmtiles show output.pmtiles
# pmtiles: pip install pmtiles でインストール

4.4 圧縮オプション

# gzip圧縮を明示的に指定(デフォルト)
tippecanoe \
  -o output.pmtiles \
  -z 14 -Z 8 \
  --no-tile-compression \   # 圧縮しない場合(デバッグ用)
  llm_access_joined.geojson

5. ベクトルタイルの可視化(MapLibre GL JS)

5.1 MapLibre GL JSとは

MapLibre GL JS はオープンソースのWebマップライブラリです。Mapbox GL JSからフォークされ、無料で使用できます。ベクトルタイルの表示、スタイルのカスタマイズ、インタラクションに優れています。

5.2 PMTilesをローカルで配信する方法

PMTilesはHTTP Range Requestで直接配信できます。ローカルで確認する場合は簡単なHTTPサーバーが必要です。

# Python の HTTP サーバー(ローカル確認用)
python3 -m http.server 8080

# または npx serve(Node.js が必要)
npx serve .

5.3 MapLibre GL JSによるタイル表示HTML

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>LLMアクセスログ ベクトルタイルマップ</title>

  <!-- MapLibre GL JS -->
  <link
    rel="stylesheet"
    href="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.css"
  />
  <script src="https://unpkg.com/maplibre-gl@3.6.2/dist/maplibre-gl.js"></script>

  <!-- PMTiles プロトコルの追加 -->
  <script src="https://unpkg.com/pmtiles@3.0.5/dist/pmtiles.js"></script>

  <style>
    body { margin: 0; padding: 0; }
    #map { position: absolute; top: 0; bottom: 0; width: 100%; }

    /* 凡例パネル */
    #legend {
      position: absolute;
      top: 10px;
      right: 10px;
      background: rgba(255, 255, 255, 0.9);
      padding: 12px 16px;
      border-radius: 6px;
      font-family: sans-serif;
      font-size: 13px;
      box-shadow: 0 2px 6px rgba(0,0,0,0.2);
    }
    #legend h4 { margin: 0 0 8px; font-size: 14px; }
    .legend-item { display: flex; align-items: center; margin-bottom: 4px; }
    .legend-color {
      width: 16px; height: 16px;
      border-radius: 3px;
      margin-right: 8px;
      flex-shrink: 0;
    }
  </style>
</head>
<body>
  <div id="map"></div>

  <!-- 凡例 -->
  <div id="legend">
    <h4>LLMアクセス数</h4>
    <div class="legend-item">
      <div class="legend-color" style="background:#fef0d9;"></div>
      <span>低(〜10件)</span>
    </div>
    <div class="legend-item">
      <div class="legend-color" style="background:#fdcc8a;"></div>
      <span>中(10〜50件)</span>
    </div>
    <div class="legend-item">
      <div class="legend-color" style="background:#fc8d59;"></div>
      <span>高(50〜100件)</span>
    </div>
    <div class="legend-item">
      <div class="legend-color" style="background:#d7301f;"></div>
      <span>非常に高い(100件〜)</span>
    </div>
  </div>

  <script>
    // PMTiles プロトコルの登録
    const protocol = new pmtiles.Protocol();
    maplibregl.addProtocol("pmtiles", protocol.tile);

    // マップの初期化
    const map = new maplibregl.Map({
      container: "map",
      style: {
        version: 8,
        sources: {
          // ベースマップ(OpenStreetMap)
          "osm-tiles": {
            type: "raster",
            tiles: ["https://tile.openstreetmap.org/{z}/{x}/{y}.png"],
            tileSize: 256,
            attribution: "© OpenStreetMap contributors"
          }
        },
        layers: [
          {
            id: "osm-base",
            type: "raster",
            source: "osm-tiles",
            minzoom: 0,
            maxzoom: 19
          }
        ]
      },
      center: [141.35, 43.05],   // 初期中心座標(札幌付近)
      zoom: 10
    });

    map.on("load", () => {
      // PMTiles ソースを追加
      map.addSource("llm-access", {
        type: "vector",
        url: "pmtiles://http://localhost:8080/llm_access_tiles.pmtiles"
        // 本番環境では実際のURLに変更
        // 例: "pmtiles://https://example.com/tiles/llm_access.pmtiles"
      });

      // ポリゴンレイヤー(行政区域ごとのアクセス数を面で表示)
      map.addLayer({
        id: "llm-access-fill",
        type: "fill",
        source: "llm-access",
        "source-layer": "llm_access",  // tippecanoeの--layerで指定した名前
        paint: {
          "fill-color": [
            "step",
            ["get", "access_count"],
            "#fef0d9",   // 0〜10
            10,  "#fdcc8a",  // 10〜50
            50,  "#fc8d59",  // 50〜100
            100, "#d7301f"   // 100〜
          ],
          "fill-opacity": 0.7
        }
      });

      // アウトラインレイヤー
      map.addLayer({
        id: "llm-access-outline",
        type: "line",
        source: "llm-access",
        "source-layer": "llm_access",
        paint: {
          "line-color": "#555",
          "line-width": 0.5,
          "line-opacity": 0.5
        }
      });

      // クリック時にポップアップ表示
      map.on("click", "llm-access-fill", (e) => {
        const props = e.features[0].properties;
        new maplibregl.Popup()
          .setLngLat(e.lngLat)
          .setHTML(`
            <strong>${props.ward_name || "地区名不明"}</strong><br/>
            アクセス数:${props.access_count || 0} 件<br/>
            面積:${props.area_km2 ? props.area_km2.toFixed(2) : "N/A"} km²
          `)
          .addTo(map);
      });

      // カーソルの変更
      map.on("mouseenter", "llm-access-fill", () => {
        map.getCanvas().style.cursor = "pointer";
      });
      map.on("mouseleave", "llm-access-fill", () => {
        map.getCanvas().style.cursor = "";
      });
    });

    // ナビゲーションコントロール
    map.addControl(new maplibregl.NavigationControl(), "top-left");
    map.addControl(new maplibregl.ScaleControl(), "bottom-left");
  </script>
</body>
</html>

6. ハンズオン手順

事前準備

# Tippecanoeのインストール確認
tippecanoe --version

# データの確認(第04回の空間結合済みGeoJSONを使用)
ls -lh data/
# 期待するファイル: llm_access_joined.geojson

# GeoJSONの構造確認
python3 -c "
import json
with open('data/llm_access_joined.geojson') as f:
    gj = json.load(f)
print('地物数:', len(gj['features']))
print('属性:', list(gj['features'][0]['properties'].keys()))
"

ステップ1: GeoJSONをPMTilesに変換

# 基本的な変換
tippecanoe \
  -o llm_access_tiles.pmtiles \
  -z 14 \
  -Z 8 \
  --layer=llm_access \
  --include=ward_name \
  --include=access_count \
  --include=area_km2 \
  data/llm_access_joined.geojson

# 変換結果の確認
ls -lh llm_access_tiles.pmtiles

ステップ2: タイルの内容確認

# PMTiles CLI ツールのインストール
pip install pmtiles

# PMTilesのメタデータ確認
pmtiles show llm_access_tiles.pmtiles

# 特定タイルの中身を確認(ズーム10のタイルを確認)
# Z=10, X=906, Y=403 はおおよそ北海道の範囲
tile-decode llm_access_tiles.pmtiles 10/906/403 2>/dev/null | python3 -m json.tool | head -50

ステップ3: ローカルHTTPサーバーの起動

# HTMLと pmtiles ファイルを同じディレクトリに配置
cp llm_access_tiles.pmtiles web/
cp map.html web/

# HTTPサーバーの起動
cd web/
python3 -m http.server 8080

ステップ4: ブラウザで確認

ブラウザで http://localhost:8080/map.html を開いてください。

確認ポイント:
- マップが表示されるか
- ベクトルタイルのポリゴンが見えるか
- ズームイン・アウトで地物が表示/非表示されるか
- クリックでポップアップが出るか

ステップ5: スタイルの調整

// map.html の paint セクションを変更して配色を試す
// 例:access_count が 0 の地物を非表示にする
map.addLayer({
  id: "llm-access-fill",
  type: "fill",
  source: "llm-access",
  "source-layer": "llm_access",
  filter: [">", ["get", "access_count"], 0],   // 0件の地物を非表示
  paint: {
    "fill-color": [ /* ... */ ],
    "fill-opacity": 0.7
  }
});

7. 演習課題(空間演習③)

評価対象課題です。 締め切りと提出方法は授業内で別途案内します。

課題内容

第04回の空間結合済みGeoJSON(または今回新たに作成したもの)を使い、Tippecanoeでpmtilesに変換し、MapLibre GL JSで表示するHTMLを作成してください。

必須成果物

  1. PMTiles ファイル(llm_access_result.pmtiles)
  2. ズームレベル Z=8〜z=14 で変換すること
  3. 必要な属性(地区名・アクセス数など最低2属性)を保持すること

  4. HTMLファイル(map_result.html)

  5. MapLibre GL JS を使用すること
  6. アクセス数に応じた配色(最低3段階の色分け)を実装すること
  7. クリックでポップアップ(地区名・アクセス数を含む)が表示されること
  8. 凡例を表示すること

提出物一覧

ファイル名 説明
llm_access_result.pmtiles Tippecanoe で生成したPMTilesファイル
map_result.html MapLibre GL JS 表示用HTML
convert.sh Tippecanoe 変換コマンドをまとめたシェルスクリプト
report.md 考察(ファイルサイズの変化、最適化オプションの効果など)100〜200字

採点基準

項目 配点
PMTilesが正しく生成されている(ズームレベル・属性) 25点
HTMLがブラウザで正しく表示される 25点
アクセス数に応じた色分けが実装されている 20点
クリックでポップアップが表示される 15点
convert.sh が再現可能なコマンドになっている 15点

ヒント

# ファイルサイズ比較(変換前後)
echo "GeoJSON サイズ:"
ls -lh data/llm_access_joined.geojson

echo "PMTiles サイズ:"
ls -lh llm_access_result.pmtiles

# 圧縮率の確認(Python)
python3 -c "
import os
geojson_size = os.path.getsize('data/llm_access_joined.geojson')
pmtiles_size = os.path.getsize('llm_access_result.pmtiles')
print(f'GeoJSON: {geojson_size/1024:.1f} KB')
print(f'PMTiles: {pmtiles_size/1024:.1f} KB')
print(f'圧縮率: {pmtiles_size/geojson_size*100:.1f}%')
"

まとめ

  • ベクトルタイル(MVT形式)は地物のジオメトリと属性を保持しながら効率的に配信できる形式で、ラスタータイルより軽量・柔軟
  • Z/X/Yのタイル座標系とズームレベルの概念を理解することが、タイル生成の最適化につながる
  • Tippecanoe を使えば大規模なGeoJSONを数行のコマンドでPMTilesに変換できる
  • --include / --exclude で属性を選択的に保持し、ファイルサイズを最小化できる
  • MapLibre GL JS と pmtiles プロトコルを組み合わせると、サーバーなしで静的ファイルからベクトルタイルを表示できる

次回予告

第07回では、プロンプトエンジニアリングの手法を使い、地理空間処理コマンドを生成するLLMの精度をどのように改善できるか学びます。


ビッグデータ応用Ⅱ〔特徴量エンジニアリング〕 第06回 © 小野原彩香