Elmとleafletでつくるgeolocationの再現機とデータ解析

　今回の記事では前記事でDBに蓄えたgeo情報を使って、移動した軌跡を地図上に再現する方法を示します。同じプロジェクトを拡張して、検索ページを追加して、検索のためのRest APIを追加します。
[Elmとleafletでつくるgeolocationの発信機・受信機 - Qiita](https://qiita.com/sand/items/18e3a4af0aca1c6376b8)


　今回の技術的なポイントは以下の通りです。


```
１．PhoenixでRest APIを実装する
２．Ecto.Queryを使ってデータを取得する
３．Timexで時間文字列をunixtimeに変換する
４．Leaflet.jsで、polylineでラインを描き、複数のマーカをgroup化する
５．Leaflet.jsでlatlng.distanceTo（）で距離を計算する
６．ElmからRest API CALLを行う
７．Elmで簡単なデータ解析を行う
```



#１．再現・データ解析ページ
　まず再現・データ解析ページ(Page2)を追加します。このページは開始時刻と終了時刻を入力すると、APIでその時間帯に歩いたデータを取得し、地図上にラインを描くというものです。抜粋したポイントの時刻もマーカで表示します。ついでにトータル距離や時間、速度、最高速度などのデータ解析を行い表示してくれます。ダイエットや健康促進にご利用いただけます？

![image.png](https://qiita-image-store.s3.amazonaws.com/0/61915/2a94e5b9-96f3-5e9a-c0d1-8cdf64c23963.png)

　最高速度は10秒間毎の速度の最大値を取っていますが、バラツキが激しいですね。10秒間でなく、せいぜい3分毎程度のものを測らないと意味がないかもしれません。

　上の地図のラインは、以下のようなDBテーブルをもとに描いています。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/61915/a1d81046-de65-d1be-d8f5-535bb9be2672.png)


　設定ですが手動で行います。routerとcontroller、viewと設定しますが全て基本的なものです。


##１－１．router

```elixir:lib/leaflet_channel_web/router.ex
  scope "/", LeafletChannelWeb do
    pipe_through :browser # Use the default browser stack

    get "/", PageController, :index
    get "/page2", Page2Controller, :redraw
  end
```

##１－２．controller

```elixir:lib/leaflet_channel_web/controllers/page2_controller.ex
defmodule LeafletChannelWeb.Page2Controller do
  use LeafletChannelWeb, :controller

  def redraw(conn, _params) do
    render conn, "page2.html"
  end
end
```

##１－３．view

```elixir:lib/leaflet_channel_web/views/page2_view.ex
defmodule LeafletChannelWeb.Page2View do
  use LeafletChannelWeb, :view
end
```


##１－４．template (leaflet.jsコード)

　テンプレートのためのディレクトリを作ります。

```
mkdir lib/leaflet_channel_web/templates/page2
```

　テンプレートですが、leaflet.jsを使って地図を表示するためのJavaScriptコードを含みます。

```html:lib/leaflet_channel_web/templates/page2/page2.html.eex
<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>
    <a href="/">トップ</a>
    <br /><br />
    <div id="elm-area"></div>
    <br /><br />
    <div id="mapid"></div>

    <script src="/js/vendor/page2.js"></script>
    <script>
        const app = Elm.Page2.embed(document.getElementById("elm-area"));
        const point0 = [35.7102, 139.8132];
        let polyline = null;
        let grp = null;
        let zoom = 15;
        let dist = -1;
        let latlng1, latlng2;

        const 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);

        mymap.setView(point0, zoom);

        app.ports.portReqGeo.subscribe( (geogeos) => {
            var obj = [];
            geogeos.forEach(function(geogeo) {
                latlng1 = L.latLng(geogeo[0].lat, geogeo[0].lng);
                latlng2 = L.latLng(geogeo[1].lat, geogeo[1].lng);
                dist = latlng1.distanceTo(latlng2);
                obj.push ({geo1: geogeo[0], geo2: geogeo[1], dist: dist});
            })
            app.ports.portResNeo.send(obj);
        })


        app.ports.portLocations.subscribe( (latlngs) => {
          if( polyline ) {
              mymap.removeLayer(polyline);
          }
          if( latlngs.length > 0 ) {
              let p1 = latlngs[0];
              mymap.setView(p1, zoom);
              polyline = L.polyline(latlngs, {color: 'red'}).addTo(mymap);
          } else {
              mymap.setView(point0, zoom);
          }
        })

        app.ports.portMarkers.subscribe( (geos) => {
          if( grp ) {
            grp.clearLayers()
          }
          grp = L.layerGroup([]);

          geos.forEach(function(geo) {
            var time = unixTime2ymd(geo.time);
            //var marker = L.marker([geo.lat,geo.lng]).addTo(mymap).bindPopup(time).openPopup();
            var marker = L.marker([geo.lat,geo.lng]).bindPopup(time).openPopup();
            grp.addLayer(marker);
          });
          grp.addTo(mymap);
        });

        function unixTime2ymd(intTime){
            // var d = new Date( intTime );
            var d = new Date( intTime * 1000 );
            var year  = d.getFullYear();
            var month = d.getMonth() + 1;
            var day  = d.getDate();
            var hour = ( '0' + d.getHours() ).slice(-2);
            var min  = ( '0' + d.getMinutes() ).slice(-2);
            var sec   = ( '0' + d.getSeconds() ).slice(-2);

            return( year + '-' + month + '-' + day + ' ' + hour + ':' + min + ':' + sec );
        }

    </script>
  </body>
</html>
```

　このテンプレートはleaflet.jsのコードが地図を表示する部分です。Elmコードからportを通してコントロールされています。Elmのport定義の抜粋を以下に示します。

```elm:portの定義
-- OUTGOING PORT
port portLocations : List (List Float) -> Cmd msg
port portMarkers : List Geo -> Cmd msg
port portReqGeo : List (Geo, Geo) -> Cmd msg

-- INCOMING PORT
port portResNeo : (List Neo -> msg) -> Sub msg
```

　それぞれのportの説明です。

```:portの説明
-- OUTGOING PORT
port portLocations : 歩いた軌跡のラインを描きます
port portMarkers : ポイントを抜粋して時刻を表示するマーカを描きます
port portReqGeo : 2地点の位置情報から距離を計算するためのリクエストです。

-- INCOMING PORT
port portResNeo : 2地点の距離の計算結果を返します。
```


　３つのOUTGOING PORTsは同じタイミングで発行されますので、１つにまとめることが可能ですが、機能が別なので分けています。それぞれのportで必要とする引数はElm側で加工してから渡しています。JavaScriptのコードは最小限に抑えて、データの加工などはElmに任せた方が全体的にスッキリと書ける気がします。

　最後に、時刻表示のマーカですが、複数表示するのでgroup化します。描き直すときにクリアーしますが、group毎一括して行えますので。

```js:app.ports.portMarkers.subscribe
        app.ports.portMarkers.subscribe( (geos) => {
          if( grp ) {
            grp.clearLayers()
          }
          grp = L.layerGroup([]);

          geos.forEach(function(geo) {
#
            grp.addLayer(marker);
          });
          grp.addTo(mymap);
        });
```

#２．Rest APIの追加

　検索ページからリクエストを投げるAPIです。これもrouterとcontoroller、viewを定義していきます。手動で設定します。

##２－１．router

```elixir:lib/leaflet_channel_web/router.ex
#
  scope "/api", LeafletChannelWeb do
    pipe_through :api
    get "/points", PointController, :index
  end
#
```


##２－２．contoroller

　ここではEcto.Queryを使ってDBからデータを取得しています。問い合わせの定義は、通常のSQLをラッピングしてElixir構造体で表現しています。Ectoの設定については前回の記事を参考にしてください。
[Elmとleafletでつくるgeolocationの発信機・受信機 - Qiita](https://qiita.com/sand/items/18e3a4af0aca1c6376b8)

```elixir:lib/leaflet_channel_web/controllers/point_controller.ex
defmodule LeafletChannelWeb.PointController do
  use LeafletChannelWeb, :controller

  alias LeafletChannel.Point

  # Imports only from/2 of Ecto.Query
  import Ecto.Query, only: [from: 2]

  def index(conn, %{"start" => start, "stop" => stop} ) do
    start = Timex.parse!(start<>" Asia/Tokyo", "%Y-%m-%d %H:%M:%S %Z" , :strftime) |> Timex.to_unix
    stop = Timex.parse!(stop<>" Asia/Tokyo", "%Y-%m-%d %H:%M:%S %Z" , :strftime) |> Timex.to_unix
    points = LeafletChannel.Repo.all(from p in Point, where: p.time > ^start and  p.time < ^stop, order_by: [desc: p.time])

    render(conn, "index.json", points: points)
  end
end
```

　Timex.parse!でTime Zoneを指定しないと９時間ずれてしまうので注意してください。%Z で指定します。

```
iex(23)> t=Timex.parse!("2013-03-05 12:30:45 Asia/Tokyo", "%Y-%m-%d %H:%M:%S %Z", :strftime)
#DateTime<2013-03-05 12:30:45+09:00 JST Asia/Tokyo>
iex(24)> t |> Timex.to_unix
1362454245
```

　timexは以下のようにしてインストールします。

```mix.exs
#
  defp deps do
    [
#
      {:timex, "~> 3.1"}
    ]
  end
#
```


```
mix deps.get
```






##２－３．view

　Elmクライアントに返すJsonを定義します。

```elixir:lib/leaflet_channel_web/views/point_view.ex
defmodule LeafletChannelWeb.PointView do
  use LeafletChannelWeb, :view
  alias LeafletChannelWeb.PointView

  def render("index.json", %{points: points}) do
    %{data: render_many(points, PointView, "point.json")}
  end

  def render("point.json", %{point: point}) do
    %{id: point.id,
      lat: point.lat,
      lng: point.lng,
      time: point.time}
  end
end
```



##２－４．Ectoあれこれ

　本筋とはあまり関係ありませんが。

　以下のコマンドでPoint テーブルを表示できます。

```
$ iex -S mix
iex(3)> LeafletChannel.Point |> LeafletChannel.Repo.all
```

　もうすこし複雑な問い合わせです。

```
alias LeafletChannel.Point

import Ecto.Query, only: [from: 2]

LeafletChannel.Repo.all(from p in Point, where: p.time > 1525055400 and  p.time < 1588224600, order_by: [desc: p.time])
```

　PostgreSQLのコマンドでも直接確認できます。

```
sudo -u postgres psql leaflet_channel_dev -c 'select * from point'
```



　開発中はDBを何度もリセットしますね。

```
mix ecto.drop
mix ecto.create
mix ecto.migrate
```


https://hexdocs.pm/ecto/Ecto.Query.html






#３．Elmの環境設定


　brunch-config.jsにPage2.elmを追加します。

```json:assets/brunch-config.js
#
    elmBrunch: {
      elmFolder: "elm",
      mainModules: ["App.elm","Page2.elm"], // Elmプログラム
      outputFolder: "../static/vendor/js"  // Elmの実行形をstaticに吐き出す
    }
#
```

（※重要）outputFolderにはvendorサブディレクトリが指定されていることに注意してください。brunchにおいてはvendorにあるfileが優先的にロードされるようです。つまりElmのjsファイルが安全にロードされます。単にoutputFolder を "../static/js"と vendor抜きで指定すると、かなりの確率でElmのjsファイルのロードに失敗してしまいます。



　以下のようなパッケージをインストールします。

```elm:assets/elm/elm-package.json
{
    "version": "1.0.0",
    "summary": "helpful summary of your project, less than 80 characters",
    "repository": "https://github.com/user/project.git",
    "license": "BSD3",
    "source-directories": [
        "."
    ],
    "exposed-modules": [],
    "dependencies": {
        "elm-lang/core": "5.1.1 <= v < 6.0.0",
        "elm-lang/html": "2.0.0 <= v < 3.0.0",
        "elm-lang/http": "1.0.0 <= v < 2.0.0",
        "elm-lang/http": "1.0.0 <= v < 2.0.0",
        "saschatimme/elm-phoenix": "0.3.0 <= v < 1.0.0"
    },
    "elm-version": "0.18.0 <= v < 0.19.0"
}
```


```
elm-github-install
```



#４．Elmプログラム

##４－１．プログラム概略
　今回のElmプログラムは、前回の記事のものよりPhoenix channelを使わない分すっきりしています。その代わりにHttpでAPIを叩いていますが。
[Elmとleafletでつくるgeolocationの発信機・受信機 - Qiita](https://qiita.com/sand/items/18e3a4af0aca1c6376b8)

　クライアントプログラムでは、以下のような仕事をします。

```
(1) DBに指定した時刻幅のgeoデータをリクエスト(API)する
(2) 歩いた地図を描き、軌跡を赤ラインで描画する
(3) 抜粋したポイントの時刻をマーカで表示する
(4)トータルの距離や時間、速度、最高速度などのデータ解析を行う
```

　(1)はPhoenixにhttpsリクエスト(API)を発行しています。
　(2) - (4) は(1)で得られた情報を加工してJavaScriptのleaflet.jsに指示を出しています。以下のportに対応しています。

```:portの説明
-- OUTGOING PORT
(2) port portLocations : 歩いた軌跡のラインを描きます
(3) port portMarkers : ポイントを抜粋して時刻を表示するマーカを描きます
(4) port portReqGeo : 2地点の位置情報から距離を計算するためのリクエストです。

-- INCOMING PORT
(4) port portResNeo : 2地点の距離の計算結果を返します。
```

　特に(4)については leaflet.jsの関数 latlng1.distanceTo(latlng2) を利用して、2ポイント間の距離を計算し、データ解析を行います。

##４－２．Elmの全コード
　それでは以下にElmの全コードを示します。

```elm:assets/elm/Page2.elm
port module Page2 exposing (..)

import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (..)
import Task exposing (Task)
import Http
import Json.Encode as E exposing (Value)
import Json.Decode as Decode

import Time exposing (Time)


minSpeed : Float
minSpeed = 50.0 -- 分速のMin。これ以下は異常値。
maxSpeed : Float
maxSpeed = 300.0 -- 分速のMax。これ以上は異常値。


-- OUTGOING PORT
port portLocations : List (List Float) -> Cmd msg
port portMarkers : List Geo -> Cmd msg
port portReqGeo : List (Geo, Geo) -> Cmd msg

-- INCOMING PORT
port portResNeo : (List Neo -> msg) -> Sub msg


geosUrl =
    "/api/points"


main =
    Html.program
        { init = init
        , update = update
        , view = view
        , subscriptions = subscriptions
        }

-- MODEL
type alias Geo =
              { id  : Int
              , lat : Float
              , lng : Float
              , time : Time
              }

type alias Neo =
              { geo1  : Geo
              , geo2 : Geo
              , dist : Float
              }

type alias Model =
              { geos : List Geo -- viewで使わないならModelに置く必要はない？
              , neos : List Neo
              , start : String
              , stop : String -- Do not use end! keyword in elixir
              }

start : String
start = "2018-05-01 06:30:00"
stop : String
stop = "2020-05-01 06:30:00"

init : ( Model, Cmd Msg )
init =
    Model [] [] start stop ! []


-- UPDATE
type Msg
    = GetGeos
    | MsgNewGeos (Result Http.Error (List Geo))
    | ResNeo (List Neo)
    | ChangeStart String
    | ChangeStop String


update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
  case msg of
    GetGeos ->
      (model,  (getGeosList model) )

    MsgNewGeos (Ok newgeos) ->
      let
          len = List.length newgeos
          _ = Debug.log "list len=" len
      in
      { model | geos=newgeos } !
         [ portLocations <| List.map latlng <| newgeos
         , portMarkers <| List.filterMap isGeoSelect <| List.indexedMap (,) <| newgeos
         , portReqGeo <| List.map2 (,) newgeos (List.drop 1 newgeos)  -- [(g1,g2),(g2,g3),(g3,g4)...]
         ]


    MsgNewGeos (Err _) ->
      (model, Cmd.none)

    ResNeo neos ->
      let
          len = List.length neos
          _ = Debug.log "neos len=" len
      in
      {model | neos=neos} ! []

    ChangeStart start ->
      { model | start=start } ! []

    ChangeStop stop ->
      { model | stop=stop } ! []


isGeoSelect (n, geo) =
    case n%18==0 of
        True  -> Just geo
        False -> Nothing


latlng : Geo -> List Float
latlng geo =
      {--let
          _ = Debug.log "geo time=" geo.time
      in--}
      [ geo.lat, geo.lng ]

getGeosList : Model -> Cmd Msg
getGeosList model =
    Http.send MsgNewGeos (requestGeos model)

requestGeos : Model -> Http.Request (List Geo)
requestGeos model =
    let
       pointsUrl = geosUrl ++ "?start=" ++ model.start ++ "&stop=" ++ model.stop
    in
    Http.get pointsUrl ( Decode.field "data" ( Decode.list geo ) )

geo : Decode.Decoder Geo
geo =
    Decode.map4 toGeo
        (Decode.field "id" Decode.int)
        (Decode.field "lat" Decode.float)
        (Decode.field "lng" Decode.float)
        (Decode.field "time" Decode.float)

toGeo : Int -> Float -> Float -> Time -> Geo
toGeo id lat lng time =
    Geo id lat lng time



-- VIEW
view : Model -> Html Msg
view model =
  div []
    [ input [ value model.start, onInput ChangeStart ] []
    , input [ value model.stop, onInput ChangeStop ] []
    , button [ onClick GetGeos ] [ text "リスト取得" ]
    , div [] ( viewNeos model )
    ]

viewNeos model =
    case (List.length model.geos)<2 || (List.length model.neos)<2 of
        True -> []
        False -> viewNeos2 model

viewNeos2 model =
    let
        neos = List.filter (\neo -> neo.dist>0) model.neos
        dist =  List.foldr (\neo acc -> neo.dist+acc) 0 neos
        diff =  getTimeDiff(model.geos)
        speed = if diff==0 then -1 else (dist/diff)*60 -- 分速
        speeds = groupAve 18 <| List.filter (\s -> s>minSpeed && s<maxSpeed) <| List.map getSpeed neos
        ave = case List.isEmpty(speeds) of
                  True -> -1
                  False -> (List.sum speeds) / toFloat(List.length(speeds))
        max = case List.maximum speeds of
                  Just m -> m
                  Nothing -> -1
        min = case List.minimum speeds of
                  Just m -> m
                  Nothing -> -1
        -- _ = Debug.log "speeds=" speeds
    in
    [ h3 [] [text "解析結果"]
    , p [] [text <| "距離(m) = "++(toString dist)]
    , p [] [text <| "時間(分) = "++(toString (diff/60))]
    , p [] [text <| "速度(分速) = "++(toString speed)]
    , p [] [text <| "速度(有効平均) = "++(toString ave)]
    , p [] [text <| "最高速度 = "++(toString max)]
    , p [] [text <| "最低速度 = "++(toString min)]
    -- , ul [] (List.map viewNeo model.neos)
    ]


getTimeDiff geos =
    let
        t1 = headTime geos
        t9 = headTime <| List.reverse geos
    in
    t1 - t9


headTime geos =
    case List.head geos of
        Just geo -> geo.time
        Nothing -> 0

getSpeed neo =
    case neo.geo1.time > neo.geo2.time of
        True  -> (neo.dist / (neo.geo1.time - neo.geo2.time)) * 60
        False -> -1

-- ある程度まとまった時間の分速を求める=> 10秒x18=3分毎の速度
groupAve : Int -> List Float -> List Float
groupAve n speeds =
    case List.isEmpty speeds of
        True -> []
        False -> let
                   head = List.take n speeds
                   ave = (List.sum head) / toFloat(List.length(head))
                   tail = List.drop n speeds
                   aves = groupAve n tail
                 in
                   ave::aves

viewNeo neo =
    li []
       [ text <| toString <| neo.geo1.time
       , text " - "
       , text <| toString <| neo.geo2.time
       , text " - "
       , text <| toString <| neo.dist
       ]


-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions model =
      Sub.batch [ portResNeo ResNeo ]
```

##４－３．Port

　以下のコードが３つのOUTGOING port呼び出しと、それぞれの引数のためのデータ加工ですが、大変スッキリ書けていると自画自賛したくなります。Elmの記述性・効率性は素晴らしい！

```elm:OUTGOING-PORT
#
    MsgNewGeos (Ok newgeos) ->
      let
          len = List.length newgeos
          _ = Debug.log "list len=" len
      in
      { model | geos=newgeos } !
         [ portLocations <| List.map latlng <| newgeos
         , portMarkers <| List.filterMap isGeoSelect <| List.indexedMap (,) <| newgeos
         , portReqGeo <| List.map2 (,) newgeos (List.drop 1 newgeos)  -- [(g1,g2),(g2,g3),(g3,g4)...]
         ]
 #
```

　以下がINCOMING portをリッスンしているsubscriptionsの定義です。

```elm:INCOMING-PORT
subscriptions : Model -> Sub Msg
subscriptions model =
      Sub.batch [ portResNeo ResNeo ]
```


##４－４．基本データ

　以下が基本的なデータ定義です。GeoはDBから取得したpoint情報を入れるためのものです。Neoは、Geoをもとにleaflet.jsに計算させた２point間の距離を入れるためのものです。

```elm:基本データ
type alias Geo =
              { id  : Int
              , lat : Float
              , lng : Float
              , time : Time
              }

type alias Neo =
              { geo1  : Geo
              , geo2 : Geo
              , dist : Float
              }
```


##４－５．Rest API CALL

　以下の部分が、PhoenixのRest APIにリクエストを投げて、DBの検索結果を受け取る部分です。インターフェースをできるだけシンプルに設計すれば、この程度ならDecoderもシンプルに書けます。

```elm:API呼び出し
#
getGeosList : Model -> Cmd Msg
getGeosList model =
    Http.send MsgNewGeos (requestGeos model)

requestGeos : Model -> Http.Request (List Geo)
requestGeos model =
    let
       pointsUrl = geosUrl ++ "?start=" ++ model.start ++ "&stop=" ++ model.stop
    in
    Http.get pointsUrl ( Decode.field "data" ( Decode.list geo ) )

geo : Decode.Decoder Geo
geo =
    Decode.map4 toGeo
        (Decode.field "id" Decode.int)
        (Decode.field "lat" Decode.float)
        (Decode.field "lng" Decode.float)
        (Decode.field "time" Decode.int)

toGeo : Int -> Float -> Float -> Int -> Geo
toGeo id lat lng time =
    Geo id lat lng time
 #
``` 


##４－６．簡単なデータ解析

　以下がGeoリストとNeoリストを元に簡単なデータ解析を行い、表示している部分です。VIEWにおいて行っています。geo情報は、歩いたり止まったりしていて、必ずしもきれいな情報になっていない部分もあります。またブラウザの制限かなとも思いますが、環境によっては、そもそも常に正常なgeolocationが取得できるとも限らないようです。実際には異常値を取り除いてより正確な「速度」を割り出す工夫も必要なのでしょう。面白い課題だと思います。


```elm:データ解析
-- VIEW
view : Model -> Html Msg
view model =
  div []
    [ input [ value model.start, onInput ChangeStart ] []
    , input [ value model.stop, onInput ChangeStop ] []
    , button [ onClick GetGeos ] [ text "リスト取得" ]
    , div [] ( viewNeos model )
    ]

viewNeos model =
    case (List.length model.geos)<2 || (List.length model.neos)<2 of
        True -> []
        False -> viewNeos2 model

viewNeos2 model =
    let
        neos = List.filter (\neo -> neo.dist>0) model.neos
        dist =  List.foldr (\neo acc -> neo.dist+acc) 0 neos
        diff =  getTimeDiff(model.geos)
        speed = if diff==0 then -1 else (dist/diff)*60 -- 分速
        speeds = groupAve 18 <| List.filter (\s -> s>minSpeed && s<maxSpeed) <| List.map getSpeed neos
        ave = case List.isEmpty(speeds) of
                  True -> -1
                  False -> (List.sum speeds) / toFloat(List.length(speeds))
        max = case List.maximum speeds of
                  Just m -> m
                  Nothing -> -1
        min = case List.minimum speeds of
                  Just m -> m
                  Nothing -> -1
        -- _ = Debug.log "speeds=" speeds
    in
    [ h3 [] [text "解析結果"]
    , p [] [text <| "距離(m) = "++(toString dist)]
    , p [] [text <| "時間(分) = "++(toString (diff/60))]
    , p [] [text <| "速度(分速) = "++(toString speed)]
    , p [] [text <| "速度(有効平均) = "++(toString ave)]
    , p [] [text <| "最高速度 = "++(toString max)]
    , p [] [text <| "最低速度 = "++(toString min)]
    -- , ul [] (List.map viewNeo model.neos)
    ]


getTimeDiff geos =
    let
        t1 = headTime geos
        t9 = headTime <| List.reverse geos
    in
    t1 - t9


headTime geos =
    case List.head geos of
        Just geo -> geo.time
        Nothing -> 0

getSpeed neo =
    case neo.geo1.time > neo.geo2.time of
        True  -> (neo.dist / (neo.geo1.time - neo.geo2.time)) * 60
        False -> -1

-- ある程度まとまった時間の分速を求める=> 10秒x18=3分毎の速度
groupAve : Int -> List Float -> List Float
groupAve n speeds =
    case List.isEmpty speeds of
        True -> []
        False -> let
                   head = List.take n speeds
                   ave = (List.sum head) / toFloat(List.length(head))
                   tail = List.drop n speeds
                   aves = groupAve n tail
                 in
                   ave::aves
``` 


　今回は以上で終了です。