Open Layers3 を使用してGPSに連動する手書きマップを作る！

#概要
手書きのマップ（イラストマップ）をGPSに連動させたいと思ったがいくら調べてもやり方が見つからなかったので自作した。
イメージとしてはディズニーランドなどのイラストマップ（パンフレットに載っているやつ）にGPSで取得した現在地をプロットしたいという感じ。（実際にはフロアマップで実装したい。）

#技術の検討（長いので飛ばしてください）
この機能の実装に当たっていろいろなプラグインやAPIなどを探したが、一発で実装できる便利なものは見つからなかった。（他にも同じことをやりたい人はいそうなのに...）
ただ、この実装を代行してくれるサービスは見つかった。笑
それが、[ストローリー](https://stroly.com/)というサービスである。
[この記事](http://jp.techcrunch.com/2017/05/17/stroly-fundraise/)に詳細が載っているが、京都のスタートアップで最近1.4億円の資金調達をしたらしい。
私も、このサービスを使ってみたがα版だからか、操作方法がよくわからなかったのと、妙にサイトが重くブラウザがフリーズしてしまい（私のPCのせいかも？）今回、利用するのはあきらめた。
そもそも、このサービスは広域のマップを作成するために画像をgooglemapなどのようにタイル化して処理をする実装のようであるが、今回私が作りたいのは非常に狭い範囲の地図（フロアマップ）でありタイル化せずに1枚の画像で実装したい。
というわけで、自分で作ることにした。

まずは、地図（画像）を表示する方法を検討した。
マップなのでズームやスライドができるような仕組みが欲しい。
そして、以下の３つの方法を検討した。

1. google mapに画像レイヤーを張り付ける。
2. leafletを使用して画像を地図として使用する
3. Open Layers3を使用して画像を地図として張り付ける。

最終的には３のOpenLayers3を使用した。
以下が１と２を採用しなかった理由である。

1．googlemap API
　・回転できない
　・無駄な要素が多くスマートでない（動作が重い）

2．leaflet
　・回転できない

そう。
googlemapもleafletも回転ができない。（衝撃の事実！！！）
今回、使用したいマップはうえが北ではないので回転させる機能が必須である。
googemapAPIやleafletは使ったことがあったので、できればこのどちらかを使用したかったが、今回は上記の理由から唯一回転できるOpenLayer3を採用した。

#参考サイト
公式のサンプル
[OpenLayers Examples static image](https://openlayers.org/en/latest/examples/static-image.html)
[OpenLayers Examples geolocation](https://openlayers.org/en/latest/examples/geolocation.html)

ブログ
[距離1kmあたりの緯度・経度の度数を計算（日本・北緯35度）](https://easyramble.com/latitude-and-longitude-per-kilometer.html)

#実装方法
##1. 画像を張り付ける
[OpenLayers Examples static image](https://openlayers.org/en/latest/examples/static-image.html)を参考にして画像をopenlayerに張り付ける。

##2. GPS情報を取得する。
[OpenLayers Examples geolocation](https://openlayers.org/en/latest/examples/geolocation.html)
を参考にしてgeolocationを使用して、GPS情報を取得。

##3．コードをうまく組み合わせる。
[OpenLayers Examples geolocation](https://openlayers.org/en/latest/examples/geolocation.html)のOSM（Open Street Map）の地図を取得しているコードを[OpenLayers Examples static image](https://openlayers.org/en/latest/examples/static-image.html)の画像を埋め込むコードに書き替える。

##4．地図とGPSを連動させる。（ポイント！！！）
まずは、連動させるうえでの方針を以下の通り決めた。
・画像サイズは1000px*1000px
・画像の中心は500px*500px
・1ピクセルは1メートル
・画像の中心のGPS座標は（34.123456, 135.123456）

次に座標をメートルに変換する方法を考える。
[距離1kmあたりの緯度・経度の度数を計算（日本・北緯35度）](https://easyramble.com/latitude-and-longitude-per-kilometer.html)を参考にした。
このサイトによると、
>日本における 1km あたりの緯度は、だいたい 0.0090133729745762 度である

>日本における 1km あたりの経度は、だいたい 0.010966404715491394 度である。

ということなので、これを変換すると、
日本における緯度1度は(1/0.0090133729745762*1000)mあり、
日本における緯度1度は(1/0.010966404715491394*1000)mあるということになる。

なので、GPSの緯度座標が1度増えれば、(1/0.0090133729745762*1000)ピクセル上に現在地を移動させればよいということ。
経度についても同様である。

そして、地図の中心にしたい座標にいるときの現在地の表示を500px*500px地点にしたいので、
画像における、現在地の座標は、
緯度（高さのピクセル数）=　現在地緯度- 中心地緯度 + 500
経度（横のピクセル数）=　現在地緯度- 中心地緯度 + 500

例えば、中心緯度（今回は34.123456）から1メートル北に行くと、
緯度（高さのピクセル数）の計算結果が501になる。

今回考えた実装方法はこんな感じである。

#実装
実際に作ってみたコードが以下の通りである。

```html
<html>
  <head>
    <title>Geolocation</title>
    <link rel="stylesheet" href="https://openlayers.org/en/v4.6.4/css/ol.css" type="text/css">
    <!-- The line below is only needed for old environments like Internet Explorer and Android 4.x -->
    <script src="https://cdn.polyfill.io/v2/polyfill.min.js?features=requestAnimationFrame,Element.prototype.classList,URL"></script>
    <script src="https://openlayers.org/en/v4.6.4/build/ol.js"></script>
  </head>
  <body>
    <div id="map" class="map"></div>
    <div id="info" style="display: none;"></div>
    <label for="track">
      track position
      <input id="track" type="checkbox"/>
    </label>
    <p>
      lat : <code id="lat"></code>&nbsp;&nbsp;
      lng : <code id="lng"></code>&nbsp;&nbsp;<br>
      pixLat : <code id="pixLat"></code>&nbsp;&nbsp;
      PixLng : <code id="pixLng"></code>&nbsp;&nbsp;
    </p>
    <script>

    var extent = [0, 0, 1000, 1000];
         var projection = new ol.proj.Projection({
           code: 'xkcd-image',
           units: 'pixels',
           extent: extent
         });

         var map = new ol.Map({
           layers: [
             new ol.layer.Image({
               source: new ol.source.ImageStatic({
                 attributions: '© <a href="http://xkcd.com/license.html">xkcd</a>',
                 url: 'https://imgs.xkcd.com/comics/online_communities.png',
                 projection: projection,
                 imageExtent: extent
               })
             })
           ],
           target: 'map',
           view: new ol.View({
             projection: projection,
             center: ol.extent.getCenter(extent),
             zoom: 2,
             maxZoom: 8
           })
         });

      var view = new ol.View({
        center: [0, 0],
        zoom: 2
      });
      //

      var geolocation = new ol.Geolocation({
        projection: view.getProjection()
      });

      function el(id) {
        return document.getElementById(id);
      }

      el('track').addEventListener('change', function() {
        geolocation.setTracking(this.checked);
      });

      // update the HTML page when the position changes.
      geolocation.on('change', function() {
        el('lat').innerText =  ol.proj.transform(geolocation.getPosition(), 'EPSG:3857', 'EPSG:4326')[0]
        el('lng').innerText =  ol.proj.transform(geolocation.getPosition(), 'EPSG:3857', 'EPSG:4326')[1]
        el('pixLat').innerText = pixCoordinates[0]
        el('pixLng').innerText = pixCoordinates[1]
      });

      // handle geolocation error.
      geolocation.on('error', function(error) {
        var info = document.getElementById('info');
        info.innerHTML = error.message;
        info.style.display = '';
      });

      var accuracyFeature = new ol.Feature();
      geolocation.on('change:accuracyGeometry', function() {
        accuracyFeature.setGeometry(geolocation.getAccuracyGeometry());
        console.log(geolocation.getAccuracyGeometry())
      });

      var positionFeature = new ol.Feature();
      positionFeature.setStyle(new ol.style.Style({
        image: new ol.style.Circle({
          radius: 8,
          fill: new ol.style.Fill({
            color: '#3399CC'
          }),
          stroke: new ol.style.Stroke({
            color: '#fff',
            width: 2
          })
        })
      }));

      geolocation.on('change:position', function() {
        var coordinates = geolocation.getPosition();
        coordinates = ol.proj.transform(coordinates, 'EPSG:3857', 'EPSG:4326')

        var centerLat = 34.123456
        var centerLng = 135.123456

        var currentLat = (1/0.0090133729745762*1000)*coordinates[1] - (1/0.0090133729745762*1000)*centerLat + 500
        var currentLng =  (1/0.010966404715491394*1000)*coordinates[0]- (1/0.010966404715491394*1000)*centerLng + 500

        pixCoordinates = [currentLat, currentLng]
        positionFeature.setGeometry(pixCoordinates ?
          new ol.geom.Point(pixCoordinates) : null);
          console.log(pixCoordinates)
      });

      new ol.layer.Vector({
        map: map,
        source: new ol.source.Vector({
          features: [positionFeature]
        })
      });
    </script>
  </body>
</html>
```

こんな感じで一応実装できました。
もっとスマートに書けそうなので、随時改善していく予定です。

#感想
ライブラリやプラグインが存在しない（多分）機能を自分で作れるようになり少し成長を感じた。
しかし、今回の実装はかなりアナログ的なやり方なので、他の技術を使えばもっとスマートに精度も高いものが作れるのではないかと感じた。
精度に関しては、細かく検証はしていないが、1000px*1000px(1km³)の範囲であれば数メートルぐらいの誤差に収まる感じではあった。（そもそもGPSの精度に限界はある？）
また、仕組み的にマップの端に行けば行くほど誤差が大きくなるような気もする。（未検証）
その辺も含めて、改善していく方法を考えていかなくてはならない。