見出し画像

マイ雨雲レーダーを実装してみた

こんにちは、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)"
/>

これで雨雲レーダーの完成です!
ソースコードが断片的でわかりづらい箇所もあったかもしれませんが、ここまで読んでいただきありがとうございました。
この記事を参考にして、自分だけのマイ雨雲レーダーを作ってみてはいかがでしょうか。


【カシオのソフトウェア採用についてはこちら】