いきなり日曜大工

好きなことを好きなだけやる

Leaflet マーカー編 #4 地名ラベルの実装

地名ラベルと web フォント

本当は一番最初に地図に入れたかったものの座標データ作成がなかなかうまくいかず何度も棚上げにしていた地名ですが、試行錯誤の末ようやくいける感じになりました。

今回は web フォント + L.DivIcon + L.Marker で本物そっくりな地名入れに挑戦します。

ゲーム内地図システムの地名表示

ゲーム内地図のスクリーンショットです。地名表示は上段が地名、下段が古代シーカー文字の2段組みになっています。

これら文字ラベルはアイコンを作ったようにスクリーンショットをトレースして画像にするとかとても現実的でないので、ゲーム内地図で使われているフォントに似たフリーのフォントを探してきてテキストを L.DivIcon で表示させることにします。

古代シーカー文字

古代シーカー文字は解読可能で、海外ファンサイトの 文字対応表 を頼りに全地名について解読を試みました。すると実は REGION, MOUNTAIN, LOCATION, WATER, TIMBER, PLACE, MAGMA, ARTIFACT のどれかであり、このタイプごとにラベルの文字サイズが決まっているらしいことがわかりました。背景によって文字が見づらいところがあり、どうしても判別できなかったもの(2,3個あった)は逆に文字サイズからタイプを推定しています。

ラベル用のフォントを探す

まずは地名用の日本語フォントを探します。フォントの形状やスタイルについて分類されているサイト Identifont さんで、本物と比較的形が似ている(と思う)フリーのフォント M+ FONTS を見つけることができました(形のわかるひらがなが少なかったので結構当て推量でがんばりました)。

古代シーカー文字のフォントは海外ファン製の勝手フォントがたくさんある中から、パブリックドメインで配布されている Sheikah Complete を使わせて頂くことに。

web 用フォントの最適化

というわけでフォントのファイルをそれぞれ頂いてきましたが、地名用の日本語フォントファイルは 1.66MB あって、このまま web フォントとして使うにはサイズが気になります。

幸い地名に使われる文字は確定しているので、フォントファイルから使う文字だけのサブセットを作成してさらに ttf から圧縮率の高いフォーマット woff2 に変換します(今回利用するフォントは改変して使用可能なことを事前に確認しています)。

参考:ウェブフォントの最適化

使う文字の抽出

Python で簡単なスクリプトなど書いて作成済みの地名データから使われている文字を書き出します。

ヘブラ地方ゲルド中央ハイオディンアッカレネーテフロタバ大雪原山脈辺境高砂漠森林丘陵平スマウ峡谷奥海湿水源西東リ湖草エ池精霊の始まり台時神殿跡黄泉川氷結麓ゼワミ秘湯岳化石北ビクムコ峰ツ南ジャザ小屋頂キピポ村ヒメ城ガ峠登口ノト兄弟岩竜骨沼ホギサモ橋シ妖古代柱群八人目英雄像ベチァボパェナ遺龍流刑処場受付室巨 入荒ケニめがねヤペ関所ダォ島迷い軍演習集落忘れ去らた交易王立研究終焉ソ雷帯セュ公園外堀船着き本丸三二書斎図坑道食堂牢部訓練展望監獄貯砕広下町物見塔聖式典ゴグズんご牧底なし笛宿門前駐屯風唄賢者朽ち千年樹闘技吊連邦鏑馬ヴ礁岸プ勇気ゾ墳根性温崖ユ裂ヨ力盆湾半滝砦絡兵ゥヌ州願・獣試岬 天花双子ふ参野清覚知恵ョ江浜里街

REGIONMUTALCWBPF

フォントファイルを変換

woff2 へのフォーマット変換とサブセット作成の両方ができるサイト Transfonter さんにお世話になりました。

変換したいフォーマットを選択、Characters に必要な文字を入力

それぞれフォントファイルを変換してもらい、劇的に小さくなりました。日本語フォントはほとんど漢字なので絞り込みの効果がすごいです。

フォントまるごと(ttf)まるごと(woff2)サブセット(woff2)
M+ 2m bold1.66MB726KB33KB
Sheikah Complete33KB6KB4KB

web フォント + L.DivIcon の地名ラベル

web フォントを使ってテキスト表示する L.DivIcon を作ります。

web フォントを使うスタイルシート

スタイルシートで web フォントとラベルの定義を作ります。

@font-face {
  font-family: 'M+ 2m bold';
  src: url(/journals/botw_demo/fonts/mplus-2m-bold-v1-subset.woff2);
}
@font-face {
  font-family: 'Sheikah Complete Regular';
  src: url(/journals/botw_demo/fonts/sheikah-complete-v1-subset.woff2);
}

.label-location {
  font-family: 'M+ 2m bold';
}
.label-sheikah {
  font-family: 'Sheikah Complete Regular';
}

web フォント + L.DivIcon

次に L.DivIcon を作ります。上段が地名、下段が古代シーカー文字になるように HTML を作成し、div には後でフォントサイズや全体の位置を調整するためのクラスをつけます。

/* ループ内で与えられるデータの例
  sheikah = 'REGION';            // 古代シーカー文字
  feature.lv = 1;                // ゲーム内地図の表示ズームレベル
  feature.name = 'ヘブラ地方';    // 地名
*/
let className = 'botw-level' + feature.lv
              + ' location-type-' + sheikah.toLowerCase();
let html = '<p class="label-location">' + feature.name + '</p>'
         + '<p class="label-sheikah">' + sheikah + '</p>';
let icon = L.divIcon({
    className: className,
    html: html,
    iconSize: null
});

実際に出力される HTML はこんなイメージです。

<div class="botw-level1 location-type-region">
  <p class="label-location">ヘブラ地方</p>
  <p class="label-sheikah">REGION</p>
</div>

地名ラベルのマーカー

L.Marker で地図に表示させます。

マーカーの仕様

地名ラベルのマーカーに期待する仕様を考えます。

  • 他の地物レイヤーと比べて常に最背面に配置される
  • マウス操作などを全スルーしたい(見えてるだけが望ましい)

専用ペインを作る

タイルレイヤーを除く他のレイヤーに対して常に最背面にするには専用のペインを作って入れるのが楽です。ペインは地図コンテナ内における各レイヤーの入れ物です。

参考: Map panes

地図コンテナにはあらかじめタイルレイヤー用、マーカー用、ツールチップ用などの「ペイン」(実体は div)が用意されていてペイン自身に重ね順(z-index)が定義されています。

<div id="map" class="leaflet-container ...">            地図コンテナ
  <div class="leaflet-pane leaflet-map-pane">           ペイン全体のコンテナ
    <div class="leaflet-pane leaflet-tile-pane">        ペイン(L.TileLayer 用)
    <div class="leaflet-pane leaflet-marker-pane">      ペイン(L.Marker 用)
      デフォルトでは L.Marker のインスタンスはここに入る
    <div class="leaflet-pane leaflet-tooltip-pane">     ペイン(L.Tooltip 用)

これは抜粋で、実際にはもっといろんなペインがあります。各種レイヤーはデフォルトのペインが決められていて、例えば L.Marker のインスタンスを地図コンテナに追加するとペインの指定がなければ markerPane に入ることになります。

ペインの中の各種レイヤーはペインの中でのみ重なり合いを設定でき、ペインを越えて重なり合うことはありません(つまるところ div と z-index の話なので)。今まで重ね順を意識しなくても地図が大体いい感じに見えていたのはそんな仕組みのおかげなのでした。

ということで、地名のラベルは別ペインにわけて背景地図(タイルレイヤー)のすぐ手前に表示させることにします。ラベル用のペインを作成します。

map.createPane('label');

これで地図コンテナの中に新しいペインが追加されるので

<div class="leaflet-pane leaflet-map-pane">
  <div class="leaflet-pane leaflet-tile-pane"></div>
  ....
  <div class="leaflet-pane leaflet-label-pane"></div>
</div>

スタイルシートで z-index をタイルレイヤーより上、マーカーより下になるように設定し、ついでにマウスイベントの対象から外しておきます。

.leaflet-label-pane {
  z-index: 300;
  pointer-events: none;
}

表示するだけのマーカー

L.Marker のオプションを確認して「インタラクティブ」な機能を切り、今作ったペインを指定します。すでにスタイルシートでもマウス操作を出来なくしていますが、いらない機能はカットしておきます。

let marker = L.marker(latLng, {
    icon: icon,
    interactive: false,
    keyboard: false,
    pane: 'label'
});

これで地物レイヤーより後ろにあってマウスやキーボードでさわれない、見えるだけマーカーができました。マーカーはゲーム内地図システムのズームレベルごとにレイヤーグループにしておきます。

ズームレベルごとの表示設定

マーカーを作成したら、ズームレベル別のレイヤーグループごとに表示の設定をします。ゲーム内地図のズームレベルは4段階なので、Leaflet 側のズームレベルと大体サイズ感が合うように対応させます。

ゲーム内地図のズームレベルLeaflet 地図のズームレベル件数
~28
~424
~6483
6より大きい107+483

やり方は前に町とお店のマーカーの表示切り替えをしたときと同じです。

CSS で位置・見た目を調整

web フォントを使った地名ラベルがズームレベルに応じて表示されるところまで来ました。

位置の調整

この状態だとマーカーの座標が生成されたラベル div の左上隅になっているので、これをラベルの横方向の中心と合わせます。

生成されるラベル div には地図上にこれを配置するための transform がインラインでついてくる(つまり div の transform は既に使われていて使えない)ので、実際の位置合わせは子要素の方のスタイルシートに設定します。

.leaflet-label-pane p {
  text-align: center;
  white-space: nowrap;
}
.label-location {
  font-family: 'M+ 2m bold';
  transform: translateX(-50%) scaleY(1.1);
}
.label-sheikah {
  font-family: 'Sheikah Complete Regular';
  transform: translateX(-50%);
}

テキストは文字列の長さや使うフォントにより画像アイコンのように事前に自分自身のサイズが決まっていないので、translateX(-50%) を使って自分自身のサイズを50%左へシフトさせることで位置を合わせます。scaleY(1.1) は本物のフォントと見比べてちょっと縦長にしたかったのでつけました。

見た目を整える

テキストの見た目を整えます。文字の色・縁取りなどの装飾をしたら、前回作ったズームレベル別にスタイルを設定できる機能を使ってズームレベルと古代シーカー文字のタイプごとにフォントの大きさを設定して、最後に縦方向の位置を微調整してできあがり。

ついでに同じ方法で町アイコンの下にも名前ラベルを追加し、デフォルトのままだったツールチップのスタイルシートや表示内容にも少々手を加えました。

BotW Demo #8 – web フォント + L.DivIcon + L.Marker で地名ラベル

これは達成感があります。地名が入ると全然雰囲気が違いますね。

L.DivIcon + L.Marker の代わりに L.Tooltip を使ってもできるんですけど両方試してみたところ、L.Marker を使ったやり方の方が総合的に見て好みだったのでこっちを採用しました。L.Tooltip でつくる場合はサブクラスを作って余計な CSS クラスをカットして紐づける座標との位置を先に調整してしまうと簡単にできると思います。

ゲーム内地図にかなり近づいてきました。あと大物のデータにはコログがありますが、細々とデータ作成中です。