【OpenStreetMap & leaflet】 配車アプリ風の地図とマーカを実装する

#概要
Uberなど配車アプリによく見られる，中心に固定されたピンに地図をスライドさせて目的地等を合わせる位置指定のUIを，OpenStreetMapとleafletを用いてweb上に実装します．

ただUberのように実装するだけでは面白くないので，JapanTaxiのアプリのように地図移動中はマーカを少し浮かせて，ピンを挿す演出を入れます．

[**動作サンプル**](https://jagimo.net/demo/osm/center_marker/index.html)

![center-marker.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/366359/5abb3b5e-b86b-21b2-4c26-c45545b8a4f2.gif)


#JavaScript & leaflet で地図周りの実装

まずはleafletの要であるJavaScript周りを実装していきます．地図やマーカの動きは全てJSに書きます．
ソースコードの全容は以下のようになります．

```center-marker.js
 /* OSM地図の設定と初期表示の緯度経度 */
  var mymap = L.map('mymap').setView([34.80977168880622 , 135.5323653370306], 15);

  /* タイルの設定（OSMの標準タイルを使用） */
  L.tileLayer('https://tiles.wmflabs.org/hikebike/{z}/{x}/{y}.png', {
    maxZoom: 19,
    attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
  }).addTo(mymap);

  /* ピンの影を黒いcircleMarkerで表現 */
  var circle = L.circleMarker(mymap.getCenter(), {
    color: 'black',
    fillColor: 'black',
    fillOpacity: 1,
    radius: 2
  }).addTo(mymap);

  /* ピンの表示（画像pin.pngを用意） */
  var pin = L.marker(mymap.getCenter(), { icon: L.icon({ iconUrl: "pin.png", iconSize: [48, 48], iconAnchor: [24,48], popupAnchor: [0, -48]}) }).addTo(mymap);
  pin.bindPopup("<p>" + mymap.getCenter().lat.toFixed(4) + "," + mymap.getCenter().lng.toFixed(4) + "<p>").openPopup(); //ピンの位置の緯度経度を表示

  /* 地図を動かしている最中はピンを少し上に浮かせる */
  mymap.on('move', function (ev) {
    var point = mymap.latLngToLayerPoint(mymap.getCenter())　//中心の緯度経度を直交座標系に変更
    var upperCenter = mymap.layerPointToLatLng([point.x, point.y - 30]); //直交座標系で少し上に移動し緯度経度に再変換

    /* 地図の動きに合わせてピンと影も追従 */
    pin.closePopup(); //移動中はポップアップを隠す
    pin.setLatLng(upperCenter);
    circle.setLatLng(mymap.getCenter());
  });

  /* 地図を動かし終わったらピンを中心に戻す */
  mymap.on('moveend', function (ev) {
    pin.setLatLng(mymap.getCenter());
    pin.bindPopup("<p>" + mymap.getCenter().lat.toFixed(4) + "," + mymap.getCenter().lng.toFixed(4) + "<p>").openPopup();
  });
}
```

OSMの地図をleafletで表示するための基本的なやり方は，[公式チュートリアル](https://leafletjs.com/examples.html)または[埼玉大学 谷謙二研究室のページ](http://ktgis.net/service/leafletlearn/index.html)が参考になります．

###マーカの設定
アイコンの表示位置は地図表示の中心の緯度経度（*mymap.getCenter()*）です．ただ，ピンの先が地図の中心である必要があるので，*iconAnchor*で表示位置のオフセットを設定しています．
また，緯度経度の表示用ポップアップはアイコンと重なってほしくないので，*popupAnchor*で表示位置を調整しています．ポップアップ内に表示する緯度経度は少数第４位までにしています．

マーカの設定部分を抜粋すると書きのようになります．

```center-marker.js
var pin = L.marker(mymap.getCenter(), { icon: L.icon({ iconUrl: "pin.png", iconSize: [48, 48], iconAnchor: [24,48], popupAnchor: [0, -48]}) }).addTo(mymap);
pin.bindPopup("<p>" + mymap.getCenter().lat.toFixed(4) + "," + mymap.getCenter().lng.toFixed(4) + "<p>").openPopup(); 
```

###地図スクロール時にマーカを浮かせる
マーカを浮かせるためには，中心の少し上にマーカの座標を再設定してやる必要があります．leafletでマーカの表示位置を指定するには緯度経度を使用しますが，「中心緯度 + y」のように緯度に定数を加算して位置をずらすのは好ましくありません．その理由は，次の２点です．

1. 地図のサイズや拡大率の変化が起きたときにマーカの移動距離が変わってしまう
2. 地図がメルカトル図法で高緯度での歪みがあるため，同じサイズ・拡大率の地図でもマーカの緯度経度に対する表示上の移動距離が異なる

なので，座標の移動を直交座標系で計算する必要があります．*latLngToLayerPoint()*を用いて一度中心座標を直交座標系に変換してから，y軸方向に座標を移動させます．そして，*layerPointToLatLng()*でy軸上方向にずらした座標を緯度経度に戻してから，移動後のマーカの座標を設定します．


地図スクロール時の動作を抜粋すると以下のようになります．

```center-marker.js
  mymap.on('move', function (ev) {
    var point = mymap.latLngToLayerPoint(mymap.getCenter())　//中心の緯度経度を直交座標系に変更
    var upperCenter = mymap.layerPointToLatLng([point.x, point.y - 30]); //直交座標系で少し上に移動し緯度経度に再変換

    /* 地図の動きに合わせてピンと影も追従 */
    pin.closePopup(); //移動中はポップアップを隠す
    pin.setLatLng(upperCenter);
    circle.setLatLng(mymap.getCenter());
  });
```

地図のスクロールが終了したイベントは*mymap.on('moveend')*で取得できますので，再びマーカに対して地図の中心座標を設定すれば，あたかもスクロールが終わったらピンを挿したかのように見えます．

```center-marker.js
  mymap.on('moveend', function (ev) {
    pin.setLatLng(mymap.getCenter());
    pin.bindPopup("<p>" + mymap.getCenter().lat.toFixed(4) + "," + mymap.getCenter().lng.toFixed(4) + "<p>").openPopup();
  });
```

#webの表示部分の実装
JSの実装が終わってしまえば，あとは簡単です．[公式チュートリアル](https://leafletjs.com/examples.html)のとおりに地図を表示するためのhtmlとcssを作っていきます．

htmlは以下のようになります．
ポイントは，leafletのjsとcssを読み込むこと（cssを先に読み込む必要がある）．そして，地図表示部のdivのidと，JSの*var mymap = L.map('hoge').setView();*の*hoge*部分のidが一致していることです．

```index.html
<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="UTF-8">
    <title>center-maker-test</title>
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.5.1/dist/leaflet.css"
      integrity="sha512-xwE/Az9zrjBIphAcBb3F6JVqxf46+CDLwfLMHloNu6KEQCAWi6HcDUbeOfBIptF7tcCzusKFjFw2yuvEpDL9wQ=="
      crossorigin=""/>
    <script src="https://unpkg.com/leaflet@1.5.1/dist/leaflet.js"
      integrity="sha512-GffPMF3RvMeYyc1LWMHtK8EbPv0iNZ8/oTtHPx9/cc2ILxQ+u905qIwdpULaqDkyBKgOaB57QTMg7ztg8Jm2Og=="
      crossorigin="">
    </script>
    <link rel="stylesheet" type="text/css" href="osm.css">
    <script type="text/javascript" src="center-marker.js"></script>
  </head>
  <body>
    <div id="mymap" style="height: 100%; width: 100vw;"></div>
  </body>
</html>
```

また今回は，地図をブラウザのサイズに対して100%表示にするために，以下のcssを定義しています．これも公式チュートリアルどおりです．

```osm.css
body {
	padding: 0;
	margin: 0;
}
html, body, #mymap {
	height: 100%;
	width: 100vw;
}
```

以上でブラウザに地図が表示できます．

#参考ページ
- Leaflet Tutorials（英語）: https://leafletjs.com/examples.html
(accessed: 2019/10/09).
- Leaflet API reference（英語）: https://leafletjs.com/reference-1.5.0.html
(accessed: 2019/10/09).