Edited at

html + js で風を可視化

More than 1 year has passed since last update.

html + js で風を可視化

ニーズがあれば書きます。 書きました。

2016.09.20 12:00 頃

台風16号接近中の風の可視化↓

https://s3-ap-northeast-1.amazonaws.com/hiyuzawa/wind_visualize/index.html


TL;DR

$ git clone https://github.com/hiyuzawa/wind_visualize.git

$ cd wind_visualize/
$ npm install
$ ./node_modules/.bin/gulp

# ブラウザで http://127.0.0.1:8000 にアクセス


気象データ


はじめに

気象データとは一般的に気温,風,気圧,降水量等のことですが、全国のアメダス観測地点に設置された計器による測定値と、それらを元に計算しメッシュ上の各地点のその値を求めたものの2種類があるようです。

今回は後者のものを利用します。

データは商用利用であればしかるべき組織より購入して利用するようですが学術的な利用であれば入手可能のようです。

以下の大学サーバ?によりホストされている

上のホームページより「データサーバ」→「気象庁データ」→「数値予報GPV」と遷移するとここが表示されるがその中のリンク数値予報GPVで今回利用するデータがダウンロードできます。

日付のリンクをたどり末端に遷移すると日別にデータが保存されています。さまざまなデータ種別がごちゃごちゃに置かれていますが、利用するのは


  • Z_C_RJTD_YYYYMMDDHH0000_MSM_GPV_RjpL-pall_FH00-15_grib2.bin

(例) Z__C_RJTD_20160922180000_MSM_GPV_Rjp_L-pall_FH00-15_grib2.bin

というデータです。これは 2016/09/22 18時(UTC) のMSMデータであり気圧面(pall)の0時間〜15時間先までの予測データであるという意味です。

MSMって何かというと少し前のページによりメソ数値予報モデルGPV (MSM) のリンク先をみるとどんなデータか雰囲気はわかると思います。ここで重要なのはこのデータは日本を中心とした格子データでありその範囲は

* 北緯22.4度~47.6度、東経120度~150度

* 0.1度×0.125度(格子数253×241) (※今回利用するのは気圧面)

その他にも今回利用しませんがGSMなどの全球(全地球)データもあるようです。ワクワクしますねw


GRIB2

さて、利用するデータを1つダウンロードしてみます

Z_C_RJTD_20160922210000_MSM_GPVRjp_L-pall_FH00-15_grib2.bin (48MB)

フォーマットはGRIB2形式と呼ばれ、国際気象機関が定める専用のフォーマット(仕様書:WMO INTERNATIONAL CODES)になります。

このデータを自力で一部を解読されている素晴らしいサイトがありましたがこれはほんとに骨が折れる作業なので今回はwgrib2というNOAA(アメリカ海洋大気庁)が提供する解読ツールを利用します。


install wgrib2

以下のようにソースをダウンロードしてmakeすると実行バイナリが作成されます.

#

# 最新のソースだと私の環境(mac)では make test でエラーになったので v2.0.4 を利用しています。
#

$ wget wget http://www.ftp.cpc.ncep.noaa.gov/wd51we/wgrib2/wgrib2.tgz.v2.0.4
$ mv wgrib2.tgz.v2.0.4 wgrib2.tgz
$ tar zxvf wgrib2.tgz
$ cd grib2
$ make
$ ./wgrib2/wgrib2 -version
v0.2.0.4 2/2016 Wesley Ebisuzaki, Reinoud Bokhorst, John Howard, Jaakko Hyvätti, Dusan Jovic, Kristian Nilssen, Karl Pfeiffer, Pablo Romero, Manfred Schwarb, Arlindo da Silva, Niklas Sondell, Sergey Varlamo

作成されたバイナリ ./wgrib2/wgrib2 を任意の場所にコピーして利用します


wgrib2の利用法

引数なしで起動して表示されるhelpで多機能さに圧倒されそうですが、今回利用する部分に限って説明します


  • wgrib2 [grib2データファイルパス]

grib2の中に格納されているデータ群がリストで表示されます。例えば上の例でダウンロードしたMSM Grib2データを与えると以下のようにこの中には552種類のデータがあることがわかります。

$ ./wgrib2 Z__C_RJTD_20160922210000_MSM_GPV_Rjp_L-pall_FH00-15_grib2.bin

1.1:0:d=2016092221:HGT:1000 mb:anl:
1.2:0:d=2016092221:UGRD:1000 mb:anl:
1.3:0:d=2016092221:VGRD:1000 mb:anl:
1.4:0:d=2016092221:TMP:1000 mb:anl:
...
1.550:0:d=2016092221:VGRD:100 mb:15 hour fcst:
1.551:0:d=2016092221:TMP:100 mb:15 hour fcst:
1.552:0:d=2016092221:VVEL:100 mb:15 hour fcst:

少しだけ解説すると例えば

1.2:0:d=2016092221:UGRD:1000 mb:anl:

はデータID 1.2 であり 1000気圧面におけるanl(観測による)UGRD(東西方向風力) の値という意味で

1.551:0:d=2016092221:TMP:100 mb:15 hour fcst:

はデータID 1.551であり100気圧面における15時間後予想のTMP(気温)の値という意味です。

GRIB2データごとに中身もことなりそれぞれのGRIB2データごとの仕様書にもとづいています

今回は先程も紹介したMSMデータ解説書にその中身も書いてありますのでその他のデータも何であるか予想つくと思います。


  • wgrib2 [grib2データファイルパス] -d [データID] -csv [出力フアイルパス]

指定したデータIDの中身が確認できます。

$ ./wgrib2 Z__C_RJTD_20160922210000_MSM_GPV_Rjp_L-pall_FH00-15_grib2.bin -d 1.2 -csv - 

1.2:0"2016-09-22 21:00:00","2016-09-22 21:00:00","UGRD","1000 mb",120,22.4,-1.55987
"2016-09-22 21:00:00","2016-09-22 21:00:00","UGRD","1000 mb",120.125,22.4,-1.48174
"2016-09-22 21:00:00","2016-09-22 21:00:00","UGRD","1000 mb",120.25,22.4,-1.38799
"2016-09-22 21:00:00","2016-09-22 21:00:00","UGRD","1000 mb",120.375,22.4,-0.997366
"2016-09-22 21:00:00","2016-09-22 21:00:00","UGRD","1000 mb",120.5,22.4,-0.309866
...

例えば1行目末尾に 120,22.4,-1.55987 とあります。これは

東経120.0度,北緯22.4度の地点の値は-1.55987ということを表しています。


  • wgrib2 [grib2データファイルパス] -d [データID] -no_header -ieee [出力フアイルパス]

今回利用する出力方法です. 中身を見るという意味では先程の -csv と同じです。ブラウザからJavaScriptで読み込ませるにデータ量を少なくするために各数値を4バイトで表現しそれを連結させます。

$ ./wgrib2 Z__C_RJTD_20160922210000_MSM_GPV_Rjp_L-pall_FH00-15_grib2.bin -d 1.2 -no_header -ieee UGRD.ieee

$ ls -l UGRD.dat
-rw-r--r-- 1 hiyuzawa staff 243892 9 23 10:56 UGRD.ieee

ファイルサイズが 格子点の数(253×241) x 4 = 243892 と一致しています。


データ作成

今回は以下のようなshを作成して読み込ませるデータを作成しました。


mkdata.sh

#!/bin/sh

DATETIME=$1

YEAR=${1:0:4}
YEAR=${1:0:4}
MONTH=${1:4:2}
DAY=${1:6:2}
wget http://database.rish.kyoto-u.ac.jp/arch/jmadata/data/gpv/original/${YEAR}/${MONTH}/${DAY}/Z__C_RJTD_${DATETIME}_MSM_GPV_Rjp_L-pall_FH00-15_grib2.bin

./wgrib2 Z__C_RJTD_${DATETIME}_MSM_GPV_Rjp_L-pall_FH00-15_grib2.bin -d 1.2 -no_header -ieee UGRD_00.ieee
./wgrib2 Z__C_RJTD_${DATETIME}_MSM_GPV_Rjp_L-pall_FH00-15_grib2.bin -d 1.3 -no_header -ieee VGRD_00.ieee
./wgrib2 Z__C_RJTD_${DATETIME}_MSM_GPV_Rjp_L-pall_FH00-15_grib2.bin -d 1.4 -no_header -ieee TMP_00.ieee

cat UGRD_00.ieee VGRD_00.ieee TMP_00.ieee > wind.ieee


UGRD,VGRD,TMP の格子状のデータを連結して一つのファイルにしたデータです。

$ ./mkdata.sh 20160922210000

$ ls -l wind.ieee
-rw-r--r-- 1 hiyuzawa staff 731676 9 23 11:05 wind.ieee # 253x241x4x3 = 731676


JavaScript 開発準備

本題に入る前にまず開発環境を整えます。最近は gulp とか ES6(ES2015) とか流行りなのでそれに習ってやります。


node (npm) 環境の準備

npm を用いてJavaScriptライブラリ等をインストールするのでまず node の環境を用意します。macでの環境整備になりますが先日記載したQiitaの別の記事

の nodebrew セットアップは参考になるかもしれません。何れにせよnodeが起動する環境を準備してください

$ npm -v

2.15.9

作業ディレクトリを作成しnpm init します。(以下はすべてこのディレクトリ内での作業です。)

$ mkdir wind_visualize

$ cd wind_visualize/
$ npm init

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

$ npm install gulp --save-dev

$ npm install gulp-webserver --save-dev
$ npm install vinyl-source-stream --save-dev

$ npm install browserify --save-dev

$ npm install babelify --save-dev

$ npm install babel-preset-es2015 --save-dev

そのあと gulpのタスクファイルを作成します。私はいつもこんな感じの ./gulpfile.js を作ります。

var gulp = require('gulp');

var browserify = require('browserify')
var webserver = require('gulp-webserver');
var source = require('vinyl-source-stream');
var babelify = require('babelify');

gulp.task('browserify', function() {
browserify('./js/index.js', {debug: true})
.transform(babelify, {presets: ["es2015"]})
.bundle()
.on("error", function(e){
console.log(e.message);
})
.pipe(source('bundle.js'))
.pipe(gulp.dest('./'))
});

gulp.task('watch', function(){
gulp.watch('./js/*.js', ['browserify'])
});

gulp.task('webserver', function(){
gulp.src('./')
.pipe(webserver({
host: '127.0.0.1',
livereload: true
}));
});

gulp.task('default', ['browserify', 'watch', 'webserver']);

bundle.jsを読み込ませた確認用index.htmljs/index.jsを準備してgulpを起動するとwebserverが立ち上がり http://127.0.0.1:8000 にてそれが確認できます。

$ ./node_modules/.bin/gulp

[14:06:33] Using gulpfile ~/tmp/wind_visualize/gulpfile.js
[14:06:33] Starting 'browserify'...
[14:06:33] Finished 'browserify' after 35 ms
[14:06:33] Starting 'watch'...
[14:06:33] Finished 'watch' after 21 ms
[14:06:33] Starting 'webserver'...
[14:06:33] Webserver started at http://127.0.0.1:8000
[14:06:33] Finished 'webserver' after 33 ms
[14:06:33] Starting 'default'...
[14:06:33] Finished 'default' after 26 μs

この状態のコードを GitHub におきます


日本地図をブラウザに描く

日本地図をブラウザに描く方法は色々ありそうですが、JSのデータビジュアライズライブラリであるD3.jsを使って描く方法が一般的なのでそれに習います。(「d3.js 日本地図」で検索)


日本地図データ

国土データ(海岸線)データが必要です。色々なサイトにてそのデータが提供されていますが今回は Natural Earthで提供されているデータを利用します. 英語ですし色々なリンクがあり混乱しますが 1:50m Cultural Vectors の 「Admin 0 – Countries」というデータを利用します。

詳しい説明は省略しますが、以下の手順でデータを作成します


  1. Natural Earth からデータをダウンロード (全世界の国の海岸線が含まれるshp(シェイプ)ファイル形式)

  2. shpファイルから必要な国のみを抽出しかつGeoJSON形式に変換

  3. GeoJSON形式では細かい表現すぎて容量が大きいのでTopoJson形式に変換

  4. JSから読み込ます場所にデータを移動

実際は以下のようにします。まず以下の2つのツールが必要です



  • GDAL - Step.2の変換で必要


  • topojson - Step.3の変換で必要

$ brew install gdal

$ npm install -g topojson

準備が整ったらデータ作成します

# Step.1

$ wget http://www.naturalearthdata.com/http//www.naturalearthdata.com/download/50m/cultural/ne_50m_admin_0_countries.zip
$ unzip ne_50m_admin_0_countries.zip

# Step.2
$ ogr2ogr -f GeoJSON -where "adm0_a3 IN ('JPN', 'KOR', 'PRK', 'TWN', 'CHN', 'RUS')" map_geo.json ne_50m_admin_0_countries.shp

# Step.3
$ topojson --id-property su_a3 -p NAME=name -p name -o map_topo.json map_geo.json
$ ls -l map_*
-rw-r--r-- 1 hiyuzawa staff 518770 9 23 17:06 map_geo.json
-rw-r--r-- 1 hiyuzawa staff 85497 9 23 17:06 map_topo.json # サイズが削減されている

# Step.4
$ mkdir ./data
$ mv map_topo.json ./data/


日本地図をブラウザに表示

さて、データ(./data/map_topo.json)も準備できたので、これをブラウザで描画します。必要なnpmパッケージを追加で導入します. (それぞれ動作確認がとれているバージョン指定で...)

$ npm install d3@3.5.17 --save-dev

$ npm install topojson@@1.6.26 --save-dev

地図を描画するJavaScriptモジュールを作ります

それ以外のファイルも修正します. 修正箇所

これで日本地図がブラウザに描画されるはずです。

(d3で日本地図を描くこの部分はソースを見ながら色々参考になるサイトがありますのでそちらで補完ください。)


風を可視化する

基本的な流れは以下のとおり。

1. 格子データを読み込む。

2. 風に見立てた一定数の粒子をランダムに配置する。粒子はfill-circleで表す

3. 粒子はデータエリア(緯度経度)内の任意の地点に存在するが、その任意地点の南北風値,東西風値,気温値はその座標が含まれる格子四隅の値から線形補間して求められる

4. 得られた値から粒子の次の位置が決定しその位置に粒子を移動させる(移動量が少なくすれば粒子の移動は軌跡にみえる)

5. 軌跡は一定後に徐々に消えるように描画すれば、各粒子は尾を引くように見える

6. 粒子には一定の生存期間を設けそれに達する、または範囲外に移動した際は再度ランダムな初期値を与える

7. 粒子を移動させながら描画を繰り返す

それぞれをプログラム解説する。完成したプログラムはGitHubに保存されている。


Step.1 格子データを読み込む。

./js/WindDataManager.js にてそれを行っている. XHRにて先に作った wind.ieee を読み込む(./dataフォルダに格納)。読み込み後に4バイト浮動小数点の連続データをJavaScriptの1次元配列に格納している.

const dataView = new DataView(buffer);

this.wind = new Float32Array(buffer.byteLength / 4);
for (let i = 0, len = this.wind.length; i < len; i++) {
this.wind[i] = dataView.getFloat32(i * 4);
}

読み込み完了後に次の処理に進みたいため、その終了後にCallback関数を呼び出し次の処理に進むようにする

./js/index.js (L11-L12)


Step.2 風に見立てた一定数の粒子をランダムに配置する。粒子はfill-circleで表す

./js/ParticleManager.js にて粒子全体 (サンプルでは2000と定義)を管理し, 各粒子は ./js/Particle.jsでインスタンス化される構成。粒子生成時に初期値を与える。粒子の描画はfill-circleで表現する


Step.3 粒子はデータエリア(緯度経度)内の任意の地点に存在するが、その任意地点の南北風値,東西風値,気温値はその座標が含まれる格子四隅の値から線形補間して求められる

データは格子点データのみであるが任意座標の値は4隅のデータから線形補間して求める


Step.4 得られた値から粒子の次の位置が決定しその位置に粒子を移動させる

各粒子の現在位置のそれぞれの値を求めて更新する。風の南北、東西方向の強さに応じて緯度経度値を変化させる。

this.lat = this.lat + 0.003 * this._calcWindPower(w, "v");

this.lon = this.lon + 0.003 * this._calcWindPower(w, "u");
this.prevtmp = this.tmp;
this.tmp = this._calcWindPower(w, "t");


Step.5 軌跡は一定後に徐々に消えるように描画すれば、各粒子は尾を引くように見える

Canvas関数のglobalCompositeOperationを利用すればすでに描画した領域に対して重ね塗りができる。これを利用して透明度を徐々に上げるような見せ方が可能。

this.ctx.fillStyle = "rgba(0, 0, 0, 0.95)";

var prev = this.ctx.globalCompositeOperation;
this.ctx.globalCompositeOperation = "destination-in";
this.ctx.fillRect(0, 0, this.cvs.width, this.cvs.height);
this.ctx.globalCompositeOperation = prev;

参考: globalCompositeOperation - html5.jp


Step.6 粒子には一定の生存期間を設けそれに達する、または範囲外に移動した際は再度ランダムな初期値を与える

この辺がその処理


Step.7 粒子を移動させながら描画を繰り返す

粒子を移動しCanvasに描きrequestAnimationFrameを繰り返し呼び出すことでアニメーションさせる

_process() {

this.particles.forEach((p) => {
// 粒子を移動させる
p.move(this.wind);
})

this._render();
requestAnimationFrame(this._process.bind(this));
}


実行

$ git clone https://github.com/hiyuzawa/wind_visualize.git

$ cd wind_visualize/
$ npm install
$ ./node_modules/.bin/gulp

# ブラウザで http://127.0.0.1:8000 にアクセス


備考

本記事やプログラムは

風データをビジュアルに表現する - Techscore Blogを多大に参考させて頂いています。