Leafletで時系列の複数GeoTIFFファイルの降雨分布データをアニメ表示する方法

この記事では、Leafletを使って時系列の複数GeoTIFFファイルの降雨分布データをアニメ表示する方法について解説していきたいと思います。この記事で示したサンプルコードを実行すると下図のようなアニメーションが表示されます。

 

プログラム実行の様子

 

1. はじめに

このサイトではこれまで、単一のGeoTIFFファイルの降雨分布データを表示する方法時系列のGeoTIFFファイルをアニメ表示する方法について解説してきました。この記事で解説する方法は、これらの方法を使います。

ここで解説する降雨分布をアニメ表示する方法は、降雨データを降雨強度範囲ごとに色分け表示し、凡例を示すチャートも表示する実用的な内容となっています。

 

2. 時系列の複数GeoTIFFファイルの降雨分布データをアニメ表示する方法

降雨強度ごとの色分け表示とそれに基づいた凡例表示には、前回の記事で解説したとおりChroma.jsというライブラリを使っています。

アニメ表示については、JavascriptのsetTimeout()メソッドを使っています。ただ、前回のアニメ表示と違うところは、1タイムステップ前のGeoTIFFデータのレイヤーを削除しているところです。そうしないと、GeoTIFF透過率を設定しているので、1タイムステップ前のGeoTIFFデータのレイヤーが下から透けて見えてしまうからです。アニメーションを停止するときは、clearTimeout()メソッドを使っています。

GeoTIFFデータのもととなったデータは気象庁速報版解析雨量GPVです。このデータはもともとGrib2というフォーマットで提供されていますが、そのままではLeafletでは表示できないので、GeoTIFFに変換しています。

本当は、Ascii-Gridフォーマットを使いたかったのですが、残念ながらLeafletは緯度経度の角度の大きさが異なるグリッドのAscii-Gridフォーマットには対応していないので、GeoTIFFフォーマットを使っています。気象庁速報版解析雨量GPVは日本付近のグリッドの東西と南北の長さがおおよそ等しくなるように緯度経度の角度の大きさを調整しています。

 

3. ソースコードの解説

LeafletはJavascriptのライブラリなので、基本的にhtml中にJavascriptのコードを埋め込む形で記述します。下記にLeafletを使って時系列の複数GeoTIFFファイルの降雨分布データを地図上にアニメーション表示するためのサンプルコード「rain_distribution_movie.html」を示します。

4~7行目は、Leafletを動かすためのリファレンスリンクです。特に6、7行目はGeoTIFFファイルを表示するためのものですので、この部分をなくすと背景地図は表示されてもGeoTIFFファイルが表示されなくなります。

9行目は色調をコントロールするChroma.jsライブラリを利用するためのリファレンスリンクです。

11~22行目で、htmlの各div要素のスタイルを定義しています。

14行目では、地図を表示する領域#mapidの縦方向の大きさを92%にして、地図の下部に動画のコントロールボタンを配置できる領域を確保しています。

16行目のlegend、19行目のlegend_planeは、それぞれ凡例と凡例の背景の領域を定義していますが、これはLeafletの予約語のようです。

36~39行目で画面上に表示する地図の中心点の緯度経度と大きさを設定しています。

42~45行目で背景地図を設定して、そのレイヤーを画面に追加しています。

48行目で縮尺表示を設定しています。ここでは画面の左下に表示する設定にしています。

51~59行目で凡例を設定しています。53行目で関数create_legend()で具体的な凡例の仕様を設定しています。関数create_legend()の本体は145~194行目で定義されています。

62~81行目で、表示する時系列のgeotiffファイル郡のファイルパスを配列geotiff_filesに代入しています。

83~96行目までが、アニメーションのアルゴリズムです。

83行目の変数geotiff_numは、配列geotiff_filesに格納されているGeoTIFFファイルのファイルパスを指し示すためのインデックスとして用いています。

84行目の変数play_motionは、91行目と94行目で代入されるTimerオブジェクトです。

87行目で当該タイムステップのGeoTIFFファイルを地図上に表示した上で、88行目の関数remove_geotiff()でタイムステップが一つ手前のGeoTIFFファイルのレイヤーを削除して地図上から消去しています。こうすることによって、前のタイムステップの画像が現ステップの画像の下から透けて見えるのを防いでいます。

85行目から定義されている関数play()の中でsetTimeoutの引数として関数play()を再帰的に呼び出すことによって繰り返し処理をしています。

インデックスgeotiff_numが配列geotiff_filesに格納されたファイルパスの個数を超えない間は、89行目のIfクローズでインクリメントし、87行目の関数show_geotiff()で次のタイムステップのGeoTIFFファイルを表示します。

インデックスgeotiff_numがgeotiff_filesに格納されたファイルパスの個数を超えた場合は、elseクローズでインデックスgeotiff_numを0にすることによってタイムステップを先頭に戻し、先頭から再びアニメーションを再生するようにしています。

91行目と94行目の1500という数値は、次のタイムステップで関数play()を実行するまでの待ち時間(ミリ秒)です。

98~100行目ではアニメーションを停止する関数stop()で、clearTimeoutメソッドを呼び出すことにより、Timerオブジェクトを消滅させて、アニメーションを停止させています。

102~135行目では単一のGeoTIFFファイルを地図上に表示する関数show_geotiff()を定義しています。前回の記事で用いた同名の関数show_geotiffとほとんど内容は同じですが、後から参照して削除できるように、129行目でGeoTIFFファイルを表示するレイヤーを配列layersの最後尾に格納した後、130行目でそのレイヤーを地図に追加し表示しています。

138~142行目の関数remove_geotiff()は現在表示されているGeoTIFFファイルの1タイムステップ前の画像レイヤーを消去するためのものです。この関数はアニメーションを再生する関数play()の中で呼び出されています。

145~194行目は凡例を作るための関数create_legend()で、前回の記事で用いたものと同じです。

 

Leafletのサンプルコード「rain_distribution_movie.html」

<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.3.1/dist/leaflet.css" integrity="sha512Rksm5RenBEKSKFjgI3a41vrjkw4EVPlJ3+OiI65vTjIdo9brlAacEuKOiQ5OFh7cOI1bkDwLqdLw3Zg0cRJLQ==" crossorigin=""/>
    <script src="https://unpkg.com/leaflet@1.3.1/dist/leaflet.js" integrity="sha512-/Nsx9X4HebavoBvEBuyp3I7od5tA0UzAxs+j83KgC8PU0kgB4XiK4Lfe4y4cgBtaRJQEIFCW+oC506aPT2L1zw==" crossorigin=""></script>
    <script src="https://unpkg.com/georaster"></script>
    <script src="https://unpkg.com/georaster-layer-for-leaflet"></script>

    <script src="https://unpkg.com/chroma-js"></script>

    <style>
      #mapid {
        width:  100%;
        height:  92%;
      }
      #legend {
        text-align: center;
      }
      #legend_plane {
        border: 1px solid silver;
      }
    </style>
  </head>
  <body>
    <div id="mapid"></div>
    <div>
      <form name="time_control">
          <input type="button" value="Start">
          <input type="button" value="Stop"><br>
          <input type="text" name="file_name" size="100" value="">
      </form>
    </div>

    <script type="text/javascript">

      let map = L.map('mapid', {
          center: [33.0, 131.0],
          zoom: 7.0,
      });
  

      let tileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
          attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors, <a href="http://creativecommons.org/licenses/by-sa/2.0/">CC-BY-SA</a>',
      });
      tileLayer.addTo(map);

      // 縮尺を表示
      L.control.scale({ maxWidth: 200, position: 'bottomleft', imperial: false }).addTo(map);

      // 地図上に凡例を表示
      L.Control.Legend = L.Control.extend({
        onAdd: function(map) {
          return create_legend();
        }
      });
      L.control.Legend = function(opts) {
        return new L.Control.Legend(opts);
      }
      L.control.Legend({ position: 'bottomright' }).addTo(map);

      // GeoTiffファイル
      let geotiff_files = [];
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630200000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630201000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630202000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630203000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630204000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630205000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630210000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630211000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630212000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630213000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630214000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630215000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630220000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630221000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630222000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630223000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630224000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630225000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");
      geotiff_files.push("./geotiff/Z__C_RJTD_20190630230000_SRF_GPV_Ggis1km_Prr60lv_Aper10min_ANAL_grib2.tif");

      let geotiff_num = 0;
      let play_motion;
      function play() {
        document.time_control.file_name.value = geotiff_files[geotiff_num];
        show_geotiff(geotiff_files[geotiff_num]);
        remove_geotiff();
        if (geotiff_num < geotiff_files.length - 1) {
            geotiff_num++;
            play_motion = setTimeout("play()",1500);
        } else {
            geotiff_num = 0;
            play_motion = setTimeout("play()",1500);
        }
      }

      function stop() {
        clearTimeout(play_motion);
      }

      let scale = chroma.scale(["#f5f7ff", "#d6e5f0", "#a4cbe5", "#81aed6", "#668ec1", "#eeef4c", "#f7b768", "#f55857", "#aa6a81", "#765a88"]).domain([0,200]).classes([0, 1, 3, 5, 10, 20, 30, 40, 50, 80, 200]);

      // 単一のGeoTIFFファイルを表示
      let layers = [];
      function show_geotiff(geotiff_file_path){
        fetch(geotiff_file_path)
          .then(response => response.arrayBuffer())
          .then(arrayBuffer => {
            parseGeoraster(arrayBuffer).then(georaster => {
              layer = new GeoRasterLayer({
                  georaster: georaster,
                  opacity: 0.6,
                  pixelValuesToColorFn: function(pixelValues) {
                    let pixelValue = pixelValues[0]; // there's just one band in this raster

                    // if there's zero wind, don't return a color
                    if (pixelValue === 0) return null;

                    // scale to 0 - 1 used by chroma
                    let color = scale(pixelValue).hex();

                    // console.log(color);
                    return color;
                  },
                  resolution: 256
              });
              // console.log("layer:", layer);
              layers.push(layer);
              layers[layers.length-1].addTo(map);

            });
          }
        );
      }

      // 一つ前のステップで追加したGeoTiffレイヤーを削除する関数
      function remove_geotiff() {
        if(layers.length > 1){
          map.removeLayer(layers[layers.length-2]);
        }
      }

      // 凡例を作る関数
      function create_legend() {
        let cs = L.DomUtil.create('canvas');
        const legend_plane_width =  100;
        const legend_plane_height = 240;
        const div_num = 100;
        const margin_left = 10;
        const margin_top = 30;
        const legend_width = 30;
        const div_height = 2;
        const tick_length = 5;
        const margin_text_lengend = 30;
        cs.width = legend_plane_width;
        cs.height = legend_plane_height;
        if (cs.getContext) {

          let ctx = cs.getContext('2d');
          let scl = chroma.scale(["#f5f7ff", "#d6e5f0", "#a4cbe5", "#81aed6", "#668ec1", "#eeef4c", "#f7b768", "#f55857", "#aa6a81", "#765a88"]).classes(10);
          ctx.fillStyle = '#ffffff';
          ctx.fillRect(0, 0, legend_plane_width, legend_plane_height);

          for(let i = 0; i < div_num; i++){
            ctx.fillStyle = scl((div_num - i)/div_num);
            ctx.fillRect(margin_left, margin_top + i * div_height, legend_width, div_height);
          }
          ctx.strokeStyle = "black";
          ctx.lineWidth = 1;

          ctx.textBaseline = 'center';
          ctx.textAlign = 'left';

          ctx.font = '12px sans-serif';
          ctx.fillStyle = 'black';
          ctx.fillText("Rainfall (mm/h)", 4, 20);
          const left_pos_top = 46;
          const top_pos = 45;
          ctx.fillText("80 -", left_pos_top, top_pos);
          ctx.textAlign = 'right';
          const left_pos_2nd = 90;
          ctx.fillText("50 - 80", left_pos_2nd, top_pos + 21);
          ctx.fillText("40 - 50", left_pos_2nd, top_pos + 42);
          ctx.fillText("30 - 40", left_pos_2nd, top_pos + 63);
          ctx.fillText("20 - 30", left_pos_2nd, top_pos + 84);
          ctx.fillText("10 - 20", left_pos_2nd, top_pos + 103);
          ctx.fillText(" 5 - 10", left_pos_2nd-1, top_pos + 122);
          ctx.fillText(" 3 -  5", left_pos_2nd-2, top_pos + 142);
          ctx.fillText(" 1 -  3", left_pos_2nd-2, top_pos + 162);
          ctx.fillText("    -  1", left_pos_2nd-2, top_pos + 180);
        }
        return cs;
      }            
    </script>
  </body>
</html>

 

4. ソースコードの動かし方

この記事で使うソースコードは、下記のリンクからダウンロードすることができます。だだ、その中にあるhtmlのソースコード「rain_distribution_movie.html」をブラウザから開くと背景の日本地図と凡例しか表示されません。

 

GeoTIFFファイルのアニメーションを表示しようと思ったら、rain_distribution_movie.htmlとGeoTIFFファイルの両方をウェブサーバー上の同一ディレクトリに配置する必要があります。

ウェブサーバーを用意するのはなかなか大変だと思いますので、ここで配布するサンプルプログラムではnode.jsというサーバサイドJavascriptのプラットフォームを用いています。よって、このサンプルプログラムを動かす前に、node.jsをインストールする必要があります。

ダウンロードしたzipファイルを解凍すると、package.jsonを始めとする3つのファイルとGeoTIFFファイルが格納されたフォルダが現れます。そのディレクトリ上でコマンドプロンプトまたはターミナル画面から下記のコマンドを打ち込みます。

npm install
npm start

1行目のコマンドを打ち込むと、node.jsの必要なモジュールがインストールされます。これには数分かかると思います。そして、2行目のコマンドを打ち込むと、下図のようにウィンドウに日本地図が表示されます。

そして、ウィンドウ下部の「Start」ボタンをクリックするとアニメーションが始まります。それと同時に、ウィンドウ最下部のテキストボックスに地図上に表示されているGeoTIFFファイルのファイルパスが表示されます。「Stop」ボタンをクリックするとアニメーションが止まります。

サンプルプログラムの操作方法
プログラム実行の様子

 

5. まとめ

この記事では、Leafletを使って時系列の複数GeoTIFFファイルの降雨分布データをアニメ表示する方法について解説しました。ポイントをまとめると下記のようになります。

  • 降雨分布と凡例の色調表示については、前回の記事と同様Chroma.jsを使って実装しています。Chroma.jsを使って、降雨強度範囲ごとに色を割り付けています。
  • 時系列降雨データのアニメーション表示はJavascriptのsetTimeoutメソッドを使って一定間隔でGeoTIFFファイルの表示を切り替えることにより行っています。アニメーションを停止するときは、clearTimeoutメソッドを使っています。
  • GeoTIFFファイルの表示では背景地図が見えるように透過率を設定しています。そうすると、前のタイムステップのGeoTIFFファイルの画像が下から透けて見えてしまうので、当該タイムステップのGeoTIFFファイルを表示するときに、同時に前のタイムステップのGeoTIFFファイル画像を消去するようにしています。

以上、最後まで読んでいただきありがとうございました。

最新情報をチェックしよう!