Elm

Elmからportを通してleaflet .jsを操る

 leaflet.jsはWeb上に地図を描くためのライブラリです。とても軽いと評判です。Reactだと専用のラッパーなどが用意されています。Phoenix Channelで作る最先端Webアプリ - 地図(Geo)拡張編。ElmからはPortを使って直接leaflet.jsを操るのが良いみたいです。leaflet公式サイト

 今回作成したプログラムは以下のAWSサイトで動作しています。できればスマホのブラウザ(chrome)でアクセスしてみてください。あなたが移動すると地図のマーカーも移動します。PCでも構いませんが移動できないので面白くないかも。
https://s3-ap-northeast-1.amazonaws.com/elm-svg/leaflet.html

 5秒ごとに自動的に現在位置を取得して、マーカーの位置を移動していきます。またzoom=15で初期化してありますが、「+」ボタン、「ー」ボタンで変更できます。

1.JavaScriptプログラム

 Portを、ElmからJavaScriptのleaflet.jsを操るためのAPIと考えます。以下の3つのAPIを用意します。

-- OUTGOING PORT
portInitCurLocation : ElmからJavaScriptにlocation初期値を渡します。
portSetCurLocation : ElmからJavaScriptにlacation現在地を設定し返すように指示します。

-- INCOMING PORT
portGetCurLocation : lacation現在地を返します。

 プログラムの流れ的には以下のように図示できます。

             portInitCurLocation
             portSetCurLocation                     portGetCurLocation
Elmプログラム      -->           JavaScriptプログラム      -->         Elmプログラム

 さてJavaScriptプログラムのために、leaflet.htmlを作成します。leaflet.jsを読み込み、地図を表示します。ElmとのインターフェースであるPortを使います。JavaScriptコードにつきましては以下のサイトを参考にさせていただきました。
leaflet入門6|地図に現在地を表示する2

leaflet.html
<!DOCTYPE HTML>
<html>
  <head>
    <title>Leaflet AND Elm</title>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css">
    <script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js"></script>
    <style type="text/css">
     <!--
      #mapid { height: 400px; width: 600px}
    -->
    </style>
  </head>
  <body>
    <div id="elm-area"></div>
    <br /><br />
    <div id="mapid"></div>

    <script src="/js/app.js"></script>

    <script>
      const app = Elm.App.embed(document.getElementById("elm-area"));
      var marker=null;
      var zoom = 15;

      app.ports.portInitCurLocation.subscribe( (model) => {
        console.log("app.ports.portSetCurLocation.subscribe n="+model.point)
        zoom = model.zoom;
        mymap.setView(model.point, zoom);
      })

      app.ports.portSetCurLocation.subscribe( (model) => {
        console.log("app.ports.portSetCurLocation.subscribe n="+model.point)
        zoom = model.zoom;
        setCurLocation()
      })

      var mymap = L.map('mapid')

      L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
        maxZoom: 18,
        attribution: 'Map data &copy; <a href="http://openstreetmap.org">OpenStreetMap</a> contributors, '
      }).addTo(mymap);

      function setCurLocation(){
        if (navigator.geolocation == false){
          alert('現在地を取得できませんでした。');
          return;
        }

        function success(e) {
          var lat  = e.coords.latitude;
          var lng = e.coords.longitude;
          mymap.setView([lat, lng], zoom);
          if(marker) {
              mymap.removeLayer(marker);
          }
          marker = L.marker([lat,lng]).addTo(mymap).bindPopup('現在地').openPopup();
          var obj = new Object();
          obj.zoom = zoom;
          obj.point = [lat, lng];
          app.ports.portGetCurLocation.send(obj);
        };

        function error() {
          alert('現在地を取得できませんでした。');
        };

        navigator.geolocation.getCurrentPosition(success, error);
      }
    </script>
  </body>
</html>

 まずportInitCurLocationはElmプログラムの初期化時にCmdとして呼ばれるものです。portSetCurLocationはElmプログラムの「現在地を取得」ボタンを押したときに呼ばれます。

      app.ports.portInitCurLocation.subscribe( (pos) => {
        console.log("app.ports.portSetCurLocation.subscribe n="+pos)
        mymap.setView(pos, 15);
      })

      app.ports.portSetCurLocation.subscribe( (pos) => {
        console.log("app.ports.portSetCurLocation.subscribe n="+pos)
        setCurLocation()
      })

 successはnavigator.geolocation.getCurrentPositionで現在地取得が成功した時に呼ばれます。現在地[lat, lng]をportGetCurLocationでElm側に返しています。

        function success(e) {
#
          app.ports.portGetCurLocation.send([lat, lng]);
        };

2.Elmプログラム

 Elmはこのプログラムのメインロジックを記述していきます。JavaScriptプログラムはPort APIの向こう側のサブルーティンにすぎません。なぜElmを使うのか? 私の場合はElmの純粋関数型のシンタックスが、可読性に優れたものに思えるからです。この点においては非常に優れています。

App.elm
port module App exposing (..)

import Html exposing (Html, button, div, text, program)
import Html.Events exposing (onClick)
import Time exposing (Time, second)


-- OUTGOING PORT
port portInitCurLocation : Model -> Cmd msg
port portSetCurLocation : Model -> Cmd msg

-- INCOMING PORT
port portGetCurLocation : (Model -> msg) -> Sub msg


main : Program Never Model Msg
main =
    program
        { init = init
        , view = view
        , update = update
        , subscriptions = subscriptions
        }

-- MODEL
type alias Point =
              (Float, Float)
type alias Model =
              { zoom : Int
              , point : Point
              }

point0 : Point
point0 = (35.7102, 139.8132)

init : ( Model, Cmd Msg )
init =
    ( Model 15 point0, portInitCurLocation (Model 15 point0) )


-- UPDATE
type Msg = SetCurLocation | GetCurLocation Model | Tick Time | Increment | Decrement

update :  Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    Tick _ ->
      (model, portSetCurLocation model)
    SetCurLocation ->
      (model, portSetCurLocation model)
    GetCurLocation newmodel ->
      (newmodel, Cmd.none)
    Increment ->
      ({ model| zoom = model.zoom + 1 }, Cmd.none)
    Decrement ->
      ({ model| zoom = model.zoom - 1 }, Cmd.none)

-- VIEW
view : Model -> Html Msg
view model =
  div []
    [ button [ onClick SetCurLocation ] [ text "現在地を取得" ]
    , div [] [ text "現在値: "
             , text (toString<|Tuple.first<|model.point)
             , text " - "
             , text (toString<|Tuple.second<|model.point) ]
    , div [] [ button [ onClick Decrement ] [ text "-" ]
             , text (toString model.zoom)
             , button [ onClick Increment ] [ text "+" ] ]
    ]



-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ portGetCurLocation GetCurLocation
        , Time.every (5 * second) Tick
        ]

 ブラウザを開くと、以下のmodelの初期値でleaflet地図が初期化されます。zoom=15で、マーカーの位置が(35.7102, 139.8132)ですね。

point0 : Point
point0 = (35.7102, 139.8132)

init : ( Model, Cmd Msg )
init =
    ( Model 15 point0, portInitCurLocation (Model 15 point0) )

 「現在地を取得」ボタンを押してもいいのですが、押さなくても5秒ごとに現在地のマーカーを更新していきます。

subscriptions model =
    Sub.batch
#
        , Time.every (5 * second) Tick
        ]

update msg model =
  case msg of
    Tick _ ->
      (model, portSetCurLocation model)

JavaScript側で取得しなおした現在地は、Portを通してElmに戻されます。

subscriptions : Model -> Sub Msg
subscriptions model =
    Sub.batch
        [ portGetCurLocation GetCurLocation
#
        ]

 最後に、Portでやり取りされるデータは、Elm recordとJavaScript objectで、Portを通るときに自動変換されることに注意してください。Portを通したElm Record と JavaScript Object の互換性 - Qiita

 必要なパッケージをインストールします

elm-package install elm-lang/html

コンパイルします。

elm-make App.elm --output app.js

 navigator.geolocationはSSL環境でのみ有効なので、AWS S3にアップロードして試します。私のアンドロイドスマホで無事表示できました。
https://s3-ap-northeast-1.amazonaws.com/elm-svg/leaflet.html

 今回は以上です。