Leaflet で Riot.js を使ってみる

<http://leafletjs.com/> と <http://riotjs.com/> を一緒に使ってみます。

# 1. シナリオ

地理院地図で **平成28年熊本地震・UAV動画** というレイヤーが公開されています。このレイヤーの実体は緯度経度と動画のタイトル、youtube へのリンク、撮影日がエンコードされた GeoJSON で以下の URL から入手が可能です。

* <https://cyberjapandata.gsi.go.jp/xyz/20160414kumamoto_0416uav/2/3/1.geojson>

[地理院地図でこのレイヤーを表示](http://maps.gsi.go.jp/#11/32.846423/130.838585/&base=pale&ls=pale%7C_20160414kumamoto_0416uav&disp=11&lcd=_20160414kumamoto_0416uav&vs=c1j0l0u0f0&d=v) すると、以下のようにポップアップに最低限の情報とリンクが表示されます。


![image](https://qiita-image-store.s3.amazonaws.com/0/82494/1e1616a3-1031-3726-830f-b0fb92c24bee.png)

ここでは、ポップアップの中身の HTML を Riot.js を使ってカスタマイズし、以下のように動画を埋め込みで再生できるようにしてみましょう。



![image](https://qiita-image-store.s3.amazonaws.com/0/82494/4cf1d2db-7969-ee9d-bdb9-2b4e5367c560.png)



# 2. コード

以下の3つのファイルを用意します。
説明を簡単にするために html と script は分離していますが、まとめてしまってもかまいません。

## html

```index.html
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>leaflet + riot</title>
  <meta name="viewport" content="initial-scale=1.0, maximum-scale=1.0" />
  <link rel="stylesheet" href="https://npmcdn.com/leaflet@1.0.0-rc.1/dist/leaflet.css" />
  <script src="https://npmcdn.com/leaflet@1.0.0-rc.1/dist/leaflet.js"></script>
  <script type="riot/tag" src="my-popup.tag"></script>
  <script src="https://cdn.jsdelivr.net/riot/2.5/riot+compiler.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/3.2.2/es6-promise.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/fetch/1.0.0/fetch.min.js"></script>
</head>

<body>
  <div id="map" style="position:absolute;top:0;left:0;bottom:0;right:0;"></div>
  <script src="script.js></script>
</body>
</html>
```

Leaflet 1.0-rc1 と riot2.5 をロードして、メインとなる script.js をロード・実行するシンプルなコードです。


`<script type="riot/tag" src="my-popup.tag"></script>` の部分が riot のカスタムタグを参照する部分ですが、 type が Javascript ではないので、通常の HTML のフローの中ではロードされません。riot のコンパイラがこのタグからカスタムタグ定義を取得してコンパイルする仕組みです。

本筋とは関係ないですが GeoJSON 取得には $.getJSON や XMLHttpRequest の代わりに [fetch-API](https://developer.mozilla.org/ja/docs/Web/API/Fetch_API) を使っています。Chrome/Firefox/Opera では標準で使えるのですが (<http://caniuse.com/#search=fetch>) 、未サポートのブラウザのために GitHub の [window.fetch polyfill](https://github.com/github/fetch) をロードしています。


## script

```script.js
var map = L.map("map").setView([35.362222, 138.731389], 5);
L.tileLayer("https://cyberjapandata.gsi.go.jp/xyz/pale/{z}/{x}/{y}.png", {
  attribution: "<a href='http://maps.gsi.go.jp/development/ichiran.html'>淡色地図 (地理院タイル)</a>"
}).addTo(map);

fetch("https://cyberjapandata.gsi.go.jp/xyz/20160414kumamoto_0416uav/2/3/1.geojson")
  .then(function(a) {
    return a.json();
  })
  .then(function(geojson) {
    var layer = L.geoJson(geojson, {
      pointToLayer: function(feature, latlng) {
        return L.marker(latlng, {
          title: feature.properties.name
        }).bindPopup(function(marker) {
          var div = document.createElement("div");
          riot.mount(div, "my-popup", marker.feature);
          return div;
        }, {
          maxWidth: 420,
          minWidth: 420
        });
      }
    });
    map.addLayer(layer).fitBounds(layer.getBounds());
  })
  .catch(function() {
    console.log(arguments);
  });
```

Leaflet で GeoJSON を地図上に展開するコードとしてはごく普通のものかと思います。

1. 地図を初期化
2. ベースマップを追加
3. GeoJSON を取得して
4. `L.geoJson` で geoJSON を処理してレイヤーに
5. `pointToLayer` 関数で GeoJSON Point を L.Marker にして、ポップアップも設定
6. レイヤーを地図に追加


通常は `bindPopup("your html here")` のように bindPopup の引数に HTML 文字列を渡すことが多いのですが、ここでは HTML Element を生成する関数を渡しています。この関数は Popup が表示される前に HTML が必要になった際に実行されます。

```js:
marker.bindPopup(function(marker) {
  var div = document.createElement("div");
  riot.mount(div, "my-popup", marker.feature);
  return div;
}, {
  maxWidth: 420,
  minWidth: 420
});
```

ここでは空の div 要素を生成して、そこに my-popup カスタムタグをマウントしています。マウント時に引数として、ひとつの GeoJSON Point そのものを渡しています。これが次の my-popup.tag で使用されます。

なお、bindPopup の第二引数には `{maxWidth:420,minWidth:420}` のような Popup Option を渡していますが、これは単にポップアップの表示領域を拡張するためです。


## tag

```html:my-popup.tag
<my-popup>
  <h1>{opts.properties.name}</h1> <span>{opts.properties["撮影日"]} 撮影</span>
  <iframe width="420" height="315" src="{src}" frameborder="0" allowfullscreen></iframe>
  <code>{opts.geometry.coordinates}</code>
  <script>
    function getYoutubeEmbed(html) {
      var div = document.createElement("div");
      div.innerHTML = html;
      var href = div.querySelector("a").href;
      if (href.indexOf("https://youtu.be/") == 0)
        return href.replace("https://youtu.be/", "https://www.youtube.com/embed/");
      else if (href.indexOf("https://www.youtube.com/watch?v=") == 0)
        return href.replace("https://www.youtube.com/watch?v=", "https://www.youtube.com/embed/");
      return null;
    }
    this.src = getYoutubeEmbed(this.opts.properties["動画"]);
  </script>
  <style scoped>
    :scope {
      display: block;
    }

    h1 {
      font-size: 10pt;
      margin: 0;
      padding: 0;
    }
  </style>
</my-popup>
```

Riot のカスタムタグとして **my-popup** というタグを定義しています。
どこかから渡されてきた GeoJSON オブジェクトをもとに、 動画再生に対応した HTML を返すのが仕事です。
GeoJSON の properties には **name**, **動画**(youtube へのリンク), **撮影日** が設定されているきまりで、youtube へのリンクをパースして embed なタグを作ってあげます。

詳しくは説明しませんが

* 冒頭部分で HTML 構造を作っているようだ
* `<script>` の中で関数を作ったり `this` に値をバインドできるようだ
* `<style>` ではどうやらこのカスタム内で有効なスタイルを書くことができるようだ
* `this` にバインドした値や `opts` のような予約されたオブジェクトが HTML 構造で参照できるようだ

といった雰囲気をつかんでいただければこのドキュメントの目的は果たせるかと。
詳しくはほかの riot の記事をご覧ください。もっといろんなことができます。

# 3. デモ

以下で動作確認できます

* <https://bl.ocks.org/frogcat/ed45f9dd07ba1867de6602329e6cfdff>

![image](https://qiita-image-store.s3.amazonaws.com/0/82494/a424be6a-6cb5-7358-3a8f-800b00547168.png)




# 4. まとめ

とりあえず Leaflet と riot を連携させる初歩をやってみました。

Leaflet だと直接 HTML を書く機会は Popup の中身くらいなもので、文字連結で済むくらいならあまり苦労はないのですが、いざ凝ったスタイルの HTML やインタラクションのある HTML を書こうとすると異常に複雑になったり、保守性が下がったり、詰んだりします。

riot を使うことで地図パートとポップアップパートを分離でき、見通しのよいコードを書きやすくなります。あとメニューやら検索ボックスやらのコンポーネントを作るのにも向いています。きっと。

UI ライブラリには riot のほかにも React などなどありますが、Leaflet の手軽さに対していずれも学習コストが高かったり、ライブラリ自体が巨大だったりする印象があります。あとフロントエンド界隈は流行が速すぎてついていくのが大変です。その中でも riot は簡単かつ軽量であることを目指して作られているそうなので、比較的 Leaflet との相性はよいのではないでしょうか。