緊急地震速報を複数対応したはなし

お久しぶりです。防災アプリ Advent Calendar 2022に引き続き、
防災アプリ Advent Calendar 2023の18日にも参加させていただきました。
前日後日には、大物の方々に挟まれておりますが、そんな方々に負けないくらいの記事を書いてみました。


自己紹介

はじめましての人も多いと思いますので、まずは自己紹介から。
「FukuokaMGNTCam」です...これはCamとしての名前ですね。
ニックネームとしては「まっげねっと」です。
自分のTwitterでよく自作の「QuapInfo」を投稿しております。

さて、今回はいろんなソフト制作に挑戦し、デバッグに持ってこれたソフトや
某件の影響もありそのまま埋蔵したソフトも多かったです。
そんないろいろあった今年ですが、今回は内容の濃い記事を書きたかったのですが、今年私は受験生。今回は少し簡素なものとなります。
が、防災アプリ2024年が作られたなら、その時はぜひ期待しておいてください★


今回の題材

ということで今年は、複数地震の処理方法について書きます。
地震情報関係なので、ggってもなかなか出てこず、半世紀くらい悩みました。
嘘ですね。1週間悩みました。
ということでまずは成果物です。

PointMonitor

2つ緊急地震速報の予報円ができていますね。青の線はP波、赤・オレンジの内部塗りつぶしの線がS波です。
P波S波は、Pl〇ySta〇ionで覚えました。わりと覚えやすいので是h((
こんな感じに複数地震が発生した場合の緊急地震速報を処理することができるようにしました。
発案から2週間くらいで構想通りになりました。


複数地震とは

名前と先ほどの画像の通りですが一応説明しますと、
地震が二つ以上同時に発生することを言います。
また、緊急地震速報も少しの時間差でも二つ以上発生すれば、複数地震とはいえると思います。
複数地震と調べると連動型地震と出てきますが、複数地震とは違いますので注意...


構成

なるべく簡単な図にしてみました ↓

graph TD; API-->EventIDが違う; API-->既存情報とEventIDが同じ; EventIDが違う-->複数地震としてList追加; 既存情報とEventIDが同じ-->該当情報を更新; 複数地震としてList追加-->すべての緊急地震速報のList; 該当情報を更新-->すべての緊急地震速報のList-->順番に一つずつ緊急地震速報を表示-->一定時間を超えた情報は削除;

という感じです。


WinFormsで書いてみる

先ほどのイメージ図にあったEventIDというのは、
緊急地震速報が電文として発表される際に含まれる情報で、一つの緊急地震速報に対し固有のEventIDを持ちます。
これを利用し、既存の情報に対し、新しく入った情報のEventIDが一致すれば、既存情報の更新とします。
逆に、既存の情報に対し、新しく入った情報のEventIDが一致しなければ、新しい緊急地震速報として追加します。


緊急地震速報の分別

まずは土台となるコードです。

List eventList = new List(); //すべての緊急地震速報を格納するList
public static EarthquakeEvent viewInfo; //表示する際に使う緊急地震速報データ
public class EarthquakeEvent
{
    public string EventID { get; set; } //EventID
    public string Title { get; set; } //緊急地震速報の予報か警報か
    //その他使用したいデータを追加する。
}
                            

そして、緊急地震速報のデータを追加する際には


EarthquakeEvent existingEvent = eventList.FirstOrDefault(e => e.EventID == eventID);
//eventList内で、eventIDと一致する既存のEarthquakeEventオブジェクトの検索

EarthquakeEvent newEvent = null;
if (existingEvent != null)//既存イベントが見つかったら、既存の更新として上書き
{
    existingEvent.Title = Json["Title"]["String"].ToString();
}
else //見つからないときは、nullを返すので、新規として判定し追加
{
    newEvent = new EarthquakeEvent
    {
        EventID = eventID,
        Title = Json["Title"]["String"].ToString()
    };
}
                        

となります。
一応簡単な説明をつけているので、意味は分かりやすくなっていると思います。
ここで複数地震と既存地震の情報の判別をして、追加・更新を行うことは出来ました。


終了判定を付ける

ですが、追加はできても削除する機能はありません。
これでは永久的に緊急地震速報を追加することになってしまいます。
なので、一定時間ごとに処理ができる場所に以下のコードを置いてみます。


if (eventList.Count > 0) //緊急地震速報のデータがある
{
    DateTime now = DateTime.Now;
    eventList.RemoveAll(eventItem =>
    {
        DateTime eventTime = DateTime.ParseExact(eventItem.EventID←最終報受信時刻でもよい, "yyyyMMddHHmmss", null);
        return (now - eventTime).TotalSeconds >= 180;
    });

    if (eventList.Count > 0)
    {
        viewInfo = eventList[0]; //表示する緊急地震速報

        if (eventList.Count > 1) //複数の緊急地震速報の場合
        {
             SwichTime.Start();
        }
    }
    else
    {
        SwichTime.Stop();
    }
}
else //緊急地震速報がない
{
    SwichTime.Stop();
}

「←最終報受信時刻でもよい」については、本番で削除してかまいません。目立たせるためにおいておいてあります。


一定ごとに表示を切り替える

ここでのSwichTime Timerは、viewInfoで表示する情報の切り替えを行うためのタイマーです。
つまり、3秒ごとに複数の緊急地震速報の表示を切り替えるためのタイマーです。

次に、viewInfoで表示する情報を周期的に切り替えるコードです。



private int currentEventIndex = 0;

private void Swich_Tick(object sender, EventArgs e) //3秒ごとに実行するタイマー
{
    try
    {
        if (eventList.Count > 0)
        {
            int currentIndexDisplay = currentEventIndex + 1;
            currentEventIndex = (currentEventIndex + 1) % eventList.Count;
            viewInfo = eventList[currentEventIndex];
        }
        else
        {
            viewInfo = null;
        }
        //表示するメソッドを実行
        //例えば描画メソッドが「Mapping()」の場合は
        //Mapping();
        //を置く。
    }
    catch (Exception ex)
    {
    }
}
                            

eventList(すべての緊急地震速報)の格納している緊急地震速報の数を調べ、
3秒ごとに増やしたりしています。
例えば、3つの緊急地震速報が同時にあった場合、
1つ目→2つ目→3つ目 という感じに順番を変えています。

最後に、切り替えられたりしているviewInfoから情報を取り出す方法については、


var eventID = viewInfo.EventID;
var title = viewInfo.Title;     
                                    

とするだけで、データが入っている限りはデータを取り出すことができます。
データが表示されない場合は、文字を表示するlabelなどで、


label1.Text = eventList.Count.ToString();
                                        

で現在eventList内に存在するデータ(EEW)の「数」を調べてみてください。
(テスト表示の場合、eventIDや最終受信時刻が指定時間を超えたためにデータ消去の可能性があります。)

動作例: 地図も震央緯度経度がわかれば簡単に描画できます。

PointMonitor PointMonitor

取得情報を変えて自動取得しています。同時描画(画像の場合だと、北海道と千葉が地図内にまとめて描画)も考えたのですが、地図の狭さ的にこうするしかありませんでした。


P波S波を描画する


発生時刻と現在時刻の差

ここまで無事に行けたら幸いです。おまけとして、P波とS波の描画をやっていきます。
もちろん、複数対応というテーマですので、波も複数描画をしていきます。

まず、「情報が更新された際に処理する所」で、以下コードを置きます。


Task[] tasks = eventList.Select(earthquakeEvent => Task.Run(() => //eventListの各要素について、非同期処理を行うTaskを生成してらのTaskを要素とする配列を作る
{
    double counts = 0; //カウントを一度0に初期化
    DateTime dt = DateTime.Now;
    var tm = dt.AddSeconds(-2);
    var time = tm.ToString("yyyyMMddHHmmss");
    string endTimestring = time;

    DateTime originTime = DateTime.ParseExact(earthquakeEvent.Origin, "yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture); //"yyyy-MM-dd HH:mm:ss"は、どの形式であるかを取得元を確認してください。このタイプのミスが多いです
    DateTime endTime = DateTime.ParseExact(endTimestring, "yyyyMMddHHmmss", null);
    TimeSpan difference = endTime - originTime; //現在時刻から発生時刻の差を求める

    counts = difference.TotalSeconds; //coutnsにその差を入れる
    TravelTimeTable newEntry = new TravelTimeTable //新しいTraveltimeTableを作り、その中にeventListから取得した情報の設定
    {
        Depth = int.Parse(earthquakeEvent.Depth),
        EventID = earthquakeEvent.EventID
    };

    lock (lockObject)//マルチスレッド環境の競合を防ぐ
    {
        var existingEntry = PSwavein.FirstOrDefault(entry => entry.EventID == earthquakeEvent.EventID); //PSwaveinから同じeventIDを持つ既存のエントリを取得

        if (existingEntry != null) //既存エントリが存在する場合はそれを更新する
        {
            existingEntry.Depth = newEntry.Depth;
            existingEntry.Counts = counts;
        }
        else //新規の情報だったら、新しく追加する
        {
            newEntry.Counts = counts;
            PSwavein.Add(newEntry);
        }
    }
})).ToArray(); 

//以下はメソッドの外に置きます
private object lockObject = new object();
List<.TravelTimeTable> PSwavein = new List<.TravelTimeTable>(); //<.TravelTimeTable>は、記事を書く際にどうしてもこうしないと書けませんでした

public class TravelTimeTable
{
    public double P { get; set; }
    public double S { get; set; }
    public int Depth { get; set; }
    public int Distance { get; set; }
    public string EventID { get; set; }
    public double Counts { get; set; }
}

このコードは、P波S波を描画するうえで、地震発生からの時間をDepthやCountsに入れます。
深さの存在意義については、震源の深さによって波が大幅に変わるからです。

深さが190km違うだけで、このようにP波S波の広がり方が変わります。深発地震について
さらに、択捉島付近の例では、地震発生から30秒後からを表示しています。
P波といっても、地上までに距離があるので、そこまでを動画にしても意味はないでしょう。


P波S波を求める

続いて、P波S波の直径距離を求めていきます。これは、走時表をもとに時間と震源の深さからP波とS波の大きさを求めてみよう (ぶっくさん 様)を使用して求めています。


private void timer1_Tick(object sender, EventArgs e) //予報円の描画頻度に関わります
{
    計算();
    //おススメ 1秒/s=1000ms, 0.5秒/s=500ms, 0.25秒/s=250ms, 0.125秒/s=125ms, 0.1秒/s=100ms
}

private void 計算()
{
    foreach (var item in PSwavein)//PSwaveinに入れた情報(EEW)の数の分だけ計算します
    {
        item.Counts += 0.89 / 10.0;
        //10.0の変更について timer1でおススメの通りに設定した場合、次のように入力してください
        //1秒/s=1.0 0.5秒/s=2.0 0.25秒/s=4.0 0.125秒/s=8.0 0.1秒/s=10.0 つまり1000(ms)を秒の1000倍で割った値になります。1000(ms)/1000x(m/s)

        //TravelTimeTableConverter.GetValueについては、「走時表をもとに時間と震源の深さからP波とS波の大きさを求めてみよう (ぶっくさん 様)」を使用しています
        (double pwave, double swave) = TravelTimeTableConverter.GetValue((item.Depth), item.Counts);

        pwave = Math.Round(pwave, 4); //四捨五入し、小数点第4位に丸める
        swave = Math.Round(swave, 4);
        item.P = pwave; //PSwaveinにP波とS波の計算結果を入れる
        item.S = swave;
    }
    try
    {
        //P波S波を描画するメソッドを置く 「メソッド名();」
    }
    catch { }
}
                        

ここまでこれば、P波S波の直径距離を求めることができました。


P波S波を描画する

最後に、求めた直径距離を使用し、予報円を描画していきます。描画速度は、「メソッド名();」と置いた部分で実行します。


private readonly object graphicsLock = new object();
private void メソッド名()
{
    Bitmap canvas = new Bitmap(pictureBox.Width, pictureBox.Height);
    using (Graphics g = Graphics.FromImage(canvas))
    {
        lock (graphicsLock)
        {
            g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;

            try
            {
                {
                    //eventListとPSwaveinを合体しそれぞれの要素を2つにマッピングし新しい匿名型のシーケンスを作る
                    var zippedLists = eventList.Zip(PSwavein, (eventItem, psWaveEntryItem) => new { EventItem = eventItem, PsWaveEntryItem = psWaveEntryItem });
                    //情報を入れる
                    foreach (var item in zippedLists)
                    {
                        double radius = item.PsWaveEntryItem.S / 2; // 半径 km 
                        double Pradius = item.PsWaveEntryItem.P / 2; // 半径 km

                        // 緯度経度から画面座標を求める
                        double px = ((double.Parse(item.EventItem.EpiLon) * 0.79 - QCenterLon) * QZoom) + QXcenter;
                        double py = ((QCenterLat - double.Parse(item.EventItem.EpiLat)) * QZoom) + (QYcenter);

                        //描画に直径として使う2つの要素を格納
                        float diameter = (float)(radius * QZoom / 2.4 / 10.0); // 直径(px)
                        float Pdiameter = (float)(Pradius * QZoom / 2.4 / 10.0); // 直径(px)

                        //描画
                        Pen pen = new Pen(Color.FromArgb(230, 100, 100), 2);
                        Pen penfo = new Pen(Color.FromArgb(230, 150, 100), 2);
                        Pen Ppen = new Pen(Color.FromArgb(100, 150, 230), 2);
                        {
                            g.FillEllipse(new SolidBrush(Color.FromArgb(20, 230, 100, 100)), (float)(px - diameter / 2f), (float)(py - diameter / 1.9f), diameter, diameter / 1.0f); // 塗りつぶし
                            g.DrawEllipse(pen, (float)(px - diameter / 2f), (float)(py - diameter / 1.9f), diameter, diameter / 1.0f); // 線の描画
                            g.DrawEllipse(Ppen, (float)(px - Pdiameter / 2f), (float)(py - Pdiameter / 1.9f), Pdiameter, Pdiameter / 1.0f); // 線の描画
                        }
                    }
                }
            }
            catch { }
        }
        //Graphicsオブジェクトの解放
        g.Dispose();
    }
    pictureBox.Image = canvas;
}
                        

記事のミスがない限りは、これでP波S波の描画は出来ました。地図自体の描画については、
WinFormsでGeojsonを描画しようを一部改造し使用しました。
(緯度経度の始点のみで使用するように改造)


自作ソフトを半公開している話

MGNTDiscordサーバーで先ほどのソフトなど、4つのソフトをデバッグしてくれる方を募集しています。

PointMonitor

上から紹介すると

名前 説明
EEWD 緊急地震速報を表示するソフト。先ほどの画像です。
EQjtc(開発停止) 台湾の緊急地震速報や国内の地震情報&EEW
TsunamiInfoText 津波情報ソフト Twitterに投稿しているアレです。
実験体 Dmdataの取得テストの実験
WeatherQuakeTelop 天気や地震情報をテロップ表示するソフト

をデバッグしてもらっています。
ですが、中々デバッグしてくれる方もおらず...

サーバーについては、ヘッダー隣のNetworkより入れますし、直接ここから入れます。
いろんな方にデバッグしてほしいので、ぜひ入ってください。(懇願)


おわりに

1年前に地図描画をすると言っていましたが、その2か月足らずで目標を達成しました。
その11か月後には、こんなソフトを作ったなんて信じられないだろうなって思っています。

さて、今回はなるべくわかりやすく書いてみたつもりです。少しでも地震アプリ開発に役立ててもらえればと思います。