2024年版 KEVi の設計(前半)
これは 防災アプリ開発 Advent Calendar 2024 の4日目の記事です。
僕は KyoshinEewViewer for ingen というアプリを趣味で開発しています。
簡単に説明すると、緊急地震速報や地震情報、津波情報などを表示するアプリです。
このアプリですがいろいろな機能を盛り込んでいるので色々工夫して実装しています。
今回の投稿はこのアプリの設計について話したいと思います。
起動からウィンドウ表示まで
まずはアプリのエントリポイントから。
C# のアプリですので、もれなく Program.cs の Main
メソッドが呼ばれます。
ここではロケールの固定と、Avalonia のインスタンスの構築と起動を行います。
ここで Avalonia から App.axaml.cs が呼ばれ、実際のアプリの初期化処理を開始します。
まずスプラッシュスクリーン用のウィンドウを表示し、設定の読み込みや多重起動の警告、初回起動時のウィザード表示とメインウィンドウの初期化を行います。
起動順序を図に表すとこのような形です。
ここから MainWindow で使用している MainViewModel が呼び出され、各種初期化処理が行われます。
アプリ全体の設計
メインウィンドウの構造として左ペインのタブ部分と、右ペインの地図があります。
いろいろな情報を表示していますが、別に日本地図を切り替えているわけではなく、地図の上に表示しているレイヤーを切り替えているだけ、という構造です。
『強震モニタ』や『地震情報』のような各タブは内部のコード名として Series
が割り当てられており、各情報は SeriesBase
という抽象クラスを継承する設計としています。
普通に意味のわからない単語選択なので機能説明などの際は 『タブ』 と称しています。
この SeriesBase
は右ペインに表示するコントロールや地図に表示するレイヤーなどをメインウィンドウで表示するために内部パラメータの変更をウィンドウ側に通知できるようになっており、メインウィンドウでは左側のメニュー選択に合わせて右側に表示したり変更通知を受け取る Series を切り替える処理を行っています。
これらのインスタンスは SeriesController
が設定で有効になっている Series のみのインスタンスを作成するようにしており、Splat のコード生成によりリフレクションを使用することなく DI(依存性注入) を行ってインスタンスを生成しています。
強震モニタ
わかるかわからないような絶妙な解説をして、強震モニタタブの説明に移ります。
『強震モニタ』タブではタイムシフトやデバッグのための内部動作再現など、同じ UI を使いつつ全く別の裏側を利用することがあるため、Series 等と同様に抽象クラス EarthquakeInformationHost
(なぜ Base
を付けなかったのかは不明) を用意し、裏の処理を丸々置き換えることができるようにしています。
この仕組みはタイムシフトなどで利用されており、揺れの検知や EEW などがしっかり分離されていることがわかるかと思います。
EEW の抽象化
リアルタイム情報は複数の EEW ソースの情報を統合して処理・表示を行います。
しかし、ほとんどの EEW ソースはリアルタイムに受信するものでタイムシフトなど再現には不要です。
そのため EEW オブジェクト自体を抽象化してしまい、各 EarthquakeInformationHost
側で情報ソースの管理を行うようにしました。これによりデバッグのために擬似的に他のソースからの EEW を再現することができます。
地震情報
地震情報はただ電文を表示するだけではなく、その電文の統合を考える必要があります。そうでなければ震源情報にすでに受信していた震度情報を被せることができません。
KEVi では各電文をフラグメント(Fragment
)として保持している情報をまとめ、イベント ID ごとに保持している情報をまとめて表示できるようにしています。
こうすることで津波情報による震源要素の発表に対しても震源要素が存在する fragment
として丸めてあげることで気軽に組み込むことができます。
このままではリストに表示しているすべての電文をメモリに保持する必要が出てきてしまうので、マップに表示する部分だけは読み込まずに電文 ID のみを記録しておきます。
今現状長周期地震動階級のマップに切り替えたりはできないのはここの実装が甘く、観測震度にのみしか対応していないためです。 今後推計震度分布図の表示実装などに合わせて強化したいと思います。
津波情報
津波情報に関しては他の情報などとは異なり、コンテキストが1つのみとして扱っています。
基本的に警報・注意報・予報の発表の次に、その情報を含んだ形で観測情報が発表されます。
特に複雑な仕組みは用意していませんが、UI に落とし込むためにモデルの作成が大変でした…。
だいたい電文の構造と同じ UI にできたかな、と思うので興味のある方は電文のフォーマットを見てみても面白いと思います。
雨雲レーダー
雨雲レーダーに関しては、特に凝った仕組みはないですが少し変わったことをしています。タイル画像の取得です。
ブラウザなどで表示する場合はブラウザ側で良い感じにやってくれますが、デスクトップアプリなのでそうもいきません。
自分で取得処理を書くことになりますが、闇雲に画像を取得しようとしてしまうと無数の接続を気象庁のWebサイトと確立することになり、気象庁に迷惑を掛けるだけでなくホストマシンのソケットを使い果たしてしまいその PC のインターネット接続自体が不安定になってしまいます。
そこで RadarImagePuller というクラス(命名は不明)を用意し、指定したスレッド数で画像の取得を並列に、順番を決めて行う実装をしています。
一応ロジックを解説しておくと、このクラスには
- DL対象追加時の通知イベント(
ManualResetEventSlim
) - DL対象のキュー
- DL中のURLリスト
- 終了フラグ
の4つの要素がポイントとなっており、まずクラスの初期化に合わせて各画像取得用のスレッドが初期化されます。
この画像取得用のスレッドは、
- 終了フラグが立っていなければ継続のループ
- キューに項目が存在するか確認
- 存在しなければ、DL対象追加時の通知イベントが呼ばれるまで待機
- イベントが呼ばれた場合は1に戻る
- 内容があればデキューを試行
- 失敗した場合は他のスレッドが取って残りのDL対象が存在しないことがわかるので1に戻る
- DL処理開始に合わせてDL中のURLリストに追加
- 画像の取得処理
- 取得が完了したことを呼び出し元に通知
- DLが完了したのでDL中リストからは削除
といった処理を行っており、DL対象をキューに積んだときはその後DL対象追加時の通知イベントを呼んでやることで待機していた画像取得用スレッドが再開し、各スレッドキューがからになるまで一連の画像取得が行われます。
終了させたいときは終了フラグを建てた状態で通知イベントを読んでやることでループ終了の条件に引っかかりスレッドは終了します。
タイル画像の描画自体については…僕のQiitaで去年紹介していますので割愛します。
まだ続きます
すみません、めっちゃ長くなってきたのと思ったよりも書く時間が無かったため前後半で分けさせてください。
お詫びにあと2回のアドベントカレンダーのネタを紹介しておくと
- 2回目: 設計のお話
- 設定画面
- ワークフロー
- 電文ソースの汎用化
- 3回目: BUFR をパースする
です。お楽しみに!