世界遺産マップクイズ

2026-03-25

はじめに

地図をタップして世界遺産の場所を当てるクイズアプリ「世界遺産マップクイズ

世界遺産検定の勉強用に自分のPCで使っていたものをデプロイしました。

demo

デモ: https://wh.tompython.com/

ソースコード: GitHub

機能

UNESCO全1,247件に対応

2025年時点でUNESCOに登録されている世界遺産1,247件(現時点)すべてを収録しています。各遺産には日本語名・英語名・所在国・座標・時代区分・登録基準・概要説明のデータを持たせています。

距離ベースのスコアリング

地図をタップして予想位置を決定すると、正解地点との距離に応じてスコアが算出されます。

  • 50km以内 → 1,000点(満点)
  • 距離に応じて指数関数的に減衰1000 × exp(-d/2000)
  • 10,000km以上 → 0点

ハヴァサイン公式で地球上の2点間距離を正確に計算しているので、極地付近の遺産でもスコアが歪みません。

間隔反復(Spaced Repetition)モード

SM-2アルゴリズムをベースにした間隔反復学習を実装しました。スコアに応じて各遺産のEasiness Factor(EF)と復習間隔を更新し、苦手な遺産ほど短い間隔で再出題されます。

  • スコア400点以上(≒距離1,800km以内): 正答扱い → 復習間隔が延びる
  • スコア400点未満: 不正答扱い → 間隔リセットで再出題

フリープレイモードも用意してあるので、気軽にランダム出題で遊ぶこともできます。

地域フィルタ

全世界を対象にすると1,247件は多すぎるので、地域で絞り込めるようにしました。

  • ヨーロッパ・北米 / アジア・太平洋 / 中南米・カリブ / アフリカ / アラブ諸国 / 日本

UNESCOの地域分類に準拠しつつ、日本だけは国フィルタとして独立させています。

問題数の選択

5・10・15・20・30問から選択可能。通勤中にサクッと5問、じっくり取り組みたいときは30問、といった使い分けができます。

学習データの管理

間隔反復の学習データはlocalStorageに保存されます。プライベートブラウジングモードでも動作するよう、ストレージの利用可否を自動検出してフォールバックする仕組みです。また、JSONファイルとしてエクスポート・インポートできるので、端末間の移行も可能です。

技術スタック

シングルHTML構成

アプリ全体を1つのHTMLファイルに収めています。フレームワークやビルドツールは使わず、Vanilla JSのみ。理由は以下の通りです:

  1. デプロイの簡素さ: 静的ファイルをホスティングするだけで完結する
  2. オフライン対応のしやすさ: Service Workerでキャッシュするファイルが最小限
  3. データ量: 1,247件の遺産データをJSの配列リテラルとして埋め込んでも約580KB。gzip圧縮すれば十分な範囲

地図表示: Leaflet + CARTO

地図ライブラリにはLeafletを採用し、タイルはCARTOのダークテーマ(dark_all)を使用しています。クイズアプリのダークUIと相性が良く、遺産の位置マーカーが視覚的に目立ちます。

PWA対応

Service WorkerとWeb App Manifestを設定し、PWA(Progressive Web App)として動作します。ホーム画面に追加すればネイティブアプリのように使えます。

実装のポイント

ハヴァサイン公式によるスコア計算

地球上の2点間距離の計算にはハヴァサイン公式を使っています。

function haversine(lat1, lng1, lat2, lng2) {
  const R = 6371;
  const dLat = (lat2 - lat1) * Math.PI / 180;
  const dLng = (lng2 - lng1) * Math.PI / 180;
  const a = Math.sin(dLat/2)**2 
          + Math.cos(lat1*Math.PI/180) * Math.cos(lat2*Math.PI/180) 
          * Math.sin(dLng/2)**2;
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
}

スコアの減衰曲線はexp(-d/2000)としました。50km以内は満点、500kmで約780点、2,000kmで約370点、5,000kmで約80点と、「大陸はあっていたけど国が違う」レベルでもそれなりにスコアが出るバランスです。

SM-2ベースの間隔反復

SuperMemoのSM-2アルゴリズムを距離スコアに適応させました。スコア0〜1,000点を品質評価(quality: 0〜5)に変換し、EFと復習間隔を更新します。

function updateSR(id, score) {
  const item = getSR(id);
  const q = scoreToQuality(score); // 900→5, 700→4, 400→3, 150→2, 50→1, 0→0
  
  if (q >= 3) {
    if (item.reps === 0) item.interval = 1;      // 初回: 1日後
    else if (item.reps === 1) item.interval = 3;  // 2回目: 3日後
    else item.interval = Math.round(item.interval * item.ef);
    item.reps++;
  } else {
    item.reps = 0;
    item.interval = 1; // リセット
  }
  
  item.ef = Math.max(1.3, item.ef + (0.1 - (5-q) * (0.08 + (5-q) * 0.02)));
  item.nextReview = Date.now() + item.interval * 86400000;
}

ストレージの自動検出

プライベートブラウジングではlocalStorageが使えないブラウザがあります。書き込みテストで利用可否を判定し、使えない場合はメモリストレージにフォールバックします。プライベートモードの場合はUI上で警告を表示し、エクスポートを促します。

Leafletのマップ管理

SPA的に画面を切り替える構成のため、DOMの差し替え時にLeafletのmapインスタンスが孤立する問題が発生しました。ensureMap()で既存のコンテナがDOMに存在するかチェックし、孤立していれば再生成する方式で解決しています。

function ensureMap() {
  if (map && !document.body.contains(map.getContainer())) {
    map.remove();
    map = null;
  }
  if (map) return;
  const el = document.getElementById('map');
  if (!el) return;
  map = L.map(el, { center: [20, 0], zoom: 2, /* ... */ });
}

おわりに

世界遺産の場所を覚えること自体が目的というよりも、地図を眺めながら「この遺産はこんな場所にあるのか」という発見を楽しめるアプリを目指しました。間隔反復で繰り返し遊んでいると、自然と地理感覚が身についてきます。


世界遺産マップクイズ

デモ
GitHubレポジトリ