マイ雨雲レーダーを実装してみた
こんにちは、4月にカシオに入社したウエマツです。
私はWebアプリケーションの開発を担当する部署に配属され、開発工程の勉強に取り組んでいます。
現在、勉強の一環で作成している天気予報の表示画面に「雨雲レーダー」を実装しようと試みたのですが、パソコンで雨雲レーダーを見ると位置情報の許可を出したり、地図に必要以上の情報が含まれていたり、少々見づらいと感じました。
そこで、本記事ではカシオ社員の利用を想定して作った、「マイ雨雲レーダー」の実装方法を紹介します。
概要
完成イメージは以下の通りです。
雨雲レーダーはカシオの拠点がある八王子市のマップに、Yahoo!の気象情報APIで取得した降水強度のデータを色分けして表示します。また、スライダーで現在から60分後までの情報を選択できます(緑のマーカーがついている位置が八王子技術センターです)。
実装方法
それでは、雨雲レーダーの実装手順を解説していきます。
①地図の描画
まずは雨雲レーダーを表示する地図を描画します。
地図や雨雲レーダーの描画には、「D3.js(Data-Driven Ducuments)」というライブラリを使用します。D3.jsは取得したデータをSVG(Scalable Vector Graphics)形式で描画するJavaScriptのライブラリです。
SVGは直線や曲線などを座標上の演算で表現する「ベクター形式」で描画するため、拡大縮小しても画質が劣化しないのが特徴です。
以下のコードで、D3をインポートします。
<script src="https://d3js.org/d3.v7.min.js"></script>
このようなコードで矩形を描けます。
const width = 400;
const height = 300;
const svg = d3.select(HTMLのタグ名).append("svg").attr("width",width).attr("height", height);
svg.append("rect")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr("fill", "none")
.attr("stroke", "black")
.attr("stroke-width", 2)
地図を書くためには、描画したい区域の座標を取得して、それらを線で繋ぐという作業が必要です。座標の取得には、日本の市区町村の地理形状データを保有している「Geoshape」というリポジトリを使用します。「Geoshape 地域名」で検索すると、境界データ(TopoJSON、GeoJSON)が取得できます。
地球は3次元の球体なので、2次元平面に投影するときは座標を変換する必要があります。このプロセスを「地図投影」といいます。D3では、地図投影を「プロジェクション関数」を使って行います。
プロジェクション関数は以下のように記述します。
const projection = d3.geoMercator()
.scale(80000) //拡大率
.center([139.296235, 35.660132])
.translate([width / 2, height / 2]);
d3.geoMercator()は、メルカトル投影法を使用することを示しています。地図を作成する際に使用される一般的な手法なので、地理の授業で耳にしたことがある方も多いと思います。
.center()は投影の中心点を設定します。今回は八王子市を描画するので、Google Mapで八王子市の中心を目視で決定し、座標を取得しました。
.translate()は投影の中心点をSVGの中央に移動するための関数です。したがって、X座標とY座標はSVGと幅と高さの半分となります。
以上でプロジェクション関数の設定は完了です。これで、地図上の座標をSVGの座標に変換できるようになったので、append関数を使って地図に緯線・経線を書いたり、任意の場所に印をつけたりして、お好みでカスタマイズしてみてください。
座標の取得が完了したので、これらを線で繋いでいきます。そのために「パス」という描画指示のようなデータを生成して、D3に与える必要があります。
以下のコードで、地理データをSVGパスに変換するパスジェネレータを設定します。
const path = d3.geoPath().projection(projection);
そして、d3.jsonで指定したURLからGeoJSONのデータを読み込んで描画します。
const svg = d3.select("svg");
d3.json("https://geoshape.ex.nii.ac.jp/city/geojson/20230101/13/13201A1968.g
eojson").then(data => {
svg.append("path") // path要素を追加
.datum(data) // データを設定
.attr("d", path) // パスデータを生成
});
地図の描画に関しては、毎回JSONを取得してから描画すると表示に時間がかかってしまうので、生成したパスデータを保存して直接描画したり、GeoJSONを軽量化したTopoJSONの利用をおすすめします。
②カラーバーの作成
続いて、雨雲レーダーのカラーバーを作成します。カラーバーの配色は気象庁の設定指針を参考にしました。
// 気象庁のカラーコード
const jmaColors = [
[242, 242, 242, 1],
[160, 210, 255, 1],
[33, 140, 255, 1],
[0, 65, 255, 1],
[250, 245, 0, 1],
[255, 153, 0, 1],
[255, 40, 0, 1],
[180, 0, 104, 1],
].map((color) => `rgba(${color[0]} ${color[1]} ${color[2]} /${color[3]})`);
const levels = [0, 1, 5, 10, 20, 30, 50]; // 軸のラベル
const width = 20;
const height = 280;
const margin = { top: 20, right: 40, bottom: 20, left: 20 }; // ラベル表示用の余白
const colorBarHeight = height / (jmaColors.length - 1); // バーの高さを均等に設定
地図と同様に、SVGの描画範囲を指定します。
const svg = d3
.select("#colorBar")
.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", `translate(${margin.left},${margin.top})`);
次にカラーバーの表示範囲を設定する関数を作成します。
const y = d3
.scaleLinear()
.domain([0, jmaColors.length - 1]) // データの入力範囲
.range([height, 0]); // データの出力範囲
なぜ、出力の範囲が反転しているのかというと、下図のようにy座標は画面の下にいくほど値が大きくなります。したがって、配列の最初の要素の高さを最大値に設定します。
そして、jmaColorsで定義した色の長方形を積み上げて描画します。
svg.selectAll("rect")
.data(jmaColors.slice(0, -1))
.enter()
.append("rect")
.attr("x", 0)
.attr("y", (d, i) => y(i + 1))
.attr("width", width)
.attr("height", colorBarHeight)
.attr("fill", (d) => d)
.attr("stroke", "black")
.attr("stroke-width", 0.5);
jmaColorsには降水量が80mm/h以上の色が設定されていますが、ここでは50㎜/hまでのバーしか表示しないので、データの範囲はjmaColors.slice(0, -1)となります。また、y座標は先ほど作成したy関数で求めます。
最後に、80mm/h以上のカラーバーを三角形で描画して、軸のラベルを設定すれば完成です。
const triangleHeight = 20;
// 三角形の描画
svg.append("path")
.attr(
"d",
// 三角形の座標(左下、上、右下)
d3.line()([
[0, y(jmaColors.length - 1)],
[width / 2, y(jmaColors.length - 1) - triangleHeight],
[width, y(jmaColors.length - 1)],
])
)
.attr("fill", jmaColors[jmaColors.length - 1])
.attr("stroke", "black")
.attr("stroke-width", 0.5);
// 軸の設定
const yAxis = d3
.axisRight(y)
.tickValues(d3.range(0, jmaColors.length))
.tickFormat((d, i) => (i === jmaColors.length - 1 ? "80" : levels[i]));
// 軸の描画
svg.append("g")
.attr("class", "axis")
.attr("transform", `translate(${width}, 0)`)
.attr("stroke-width", 0.5)
.call(yAxis);
③降水強度の取得
地図とカラーバーが完成したので、降水強度を取得していきます。
まずは、地図の左上と右下の座標を求めます。
const topLeftCoordinates = projection.invert([0, 0]);
const bottomRightCoordinates = projection.invert([width, height])
そして、地図の分割数を決めて、グリッドの幅を求めます。分割数が多いほど詳細に情報が取得できますが、その分表示にも時間がかかるので注意しましょう。
const div = 10; // 分割数
const lonGrid = (bottomRightCoordinates[0] - topLeftCoordinates[0]) / div;
const latGrid = (topLeftCoordinates[1] - bottomRightCoordinates[1]) / div;
各グリッドの中心座標の降水強度を求めたいので、以下の二重ループで座標を求めます。今回はAPIの処理は記述しませんが、Yahoo!の気象情報APIは(経度、緯度)の形式で座標を10個までまとめて送信できるので、データを10個ずつ区切ります。
// 座標を10データごとに区切る
const coordinates = [];
let chunk = [];
for (
let lat = topLeftCoordinates[1] - latGrid / 2;
bottomRightCoordinates[1] <= lat;
lat -= latGrid
) {
for (
let lon = topLeftCoordinates[0] + lonGrid / 2;
lon <= bottomRightCoordinates[0];
lon += lonGrid
) {
chunk.push(`${lon},${lat}`);
if (chunk.length === 10) {
coordinates.push(chunk.join(" "));
chunk = [];
}
}
}
// 残りのデータを追加
if (chunk.length > 0) {
coordinates.push(chunk.join(" "));
}
降水強度を取得した地点は以下の図のとおりです。
④雨雲レーダーの表示
最後にAPIから取得したデータを地図に描画します。Yahoo!の気象情報APIは現在から60分後までのデータが取得できます。したがって、データは次のような形式で配列に格納したものとします。
rainfallData:[(lon, lat), 現在の降水強度, 10分後の予想降水強度..., 60分後の予想降水強度]
これらのデータを配列から1つずつ取り出して、緯度と経度を取得し、降水強度から色を判定します。そして、分割した領域に矩形を描画します。x座標とy座標は矩形の左上の頂点になるように調整して、幅と高さを求めます。
const svg = d3.select(タグ名);
svg.selectAll(".rainfall-plot").remove();
for (let i = 0; i < rainfallData.length; i += 1) {
const data = rainfallData[i];
const coordString = data[0];
const rainfall = data[index];
const color = getColorForRainfall(rainfall);
const [lon, lat] = coordString.split(",").map(Number);
const [x, y] = projection([lon - lonGrid / 2, lat + latGrid / 2]);
const [w, h] = projection([lon + lonGrid / 2, lat - latGrid / 2]);
// 降水量のプロット
svg.append("rect")
.attr("x", x)
.attr("y", y)
.attr("width", w - x)
.attr("height", Math.abs(h - y))
.attr("fill", color)
.attr("stroke", "none")
.attr("class", "rainfall-plot");
}
function getColorForRainfall(rainfall) {
if (rainfall >= 80) return jmaColors[7];
if (rainfall >= 50) return jmaColors[6];
if (rainfall >= 30) return jmaColors[5];
if (rainfall >= 20) return jmaColors[4];
if (rainfall >= 10) return jmaColors[3];
if (rainfall >= 5) return jmaColors[2];
if (rainfall >= 1) return jmaColors[1];
if (rainfall > 0) return jmaColors[0];
return "none";
}
下記のようにスライダーの値を取得して、表示するデータを切り替えることができます。
<input
type="range"
id="timeSlider"
min="0"
max="60"
step="10"
value="0"
oninput="displayRainRadar(rainfallData, this.value / 10 + 1)"
/>
これで雨雲レーダーの完成です!
ソースコードが断片的でわかりづらい箇所もあったかもしれませんが、ここまで読んでいただきありがとうございました。
この記事を参考にして、自分だけのマイ雨雲レーダーを作ってみてはいかがでしょうか。
【カシオのソフトウェア採用についてはこちら】