1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GPSログを地図で読み解く — WalkLoggerのWPFアプリ設計

1
Posted at

はじめに

前回の M5Stack CoreS3でGPSログを安全に記録する — WalkLoggerのファームウェア設計 では、CoreS3 側の GPS(Global Positioning System)記録を扱いました。続編となる今回は、私が開発している WalkLogger の Windows 側に焦点を当て、ログを受け取り、地図で読み、セッションを編集する構成を見ます📍

対象は C# / .NET の基本を知っている開発者です。ファームウェアの手順は繰り返さず、アプリプロジェクト設定などを根拠に Windows 側の設計を追います🧭

image.png

image.png

今回のゴール

  • 記録ファイルがインポートされ、セッションとして画面に届くまでの経路
  • Domain/Core、Application、Infrastructure.Windows、Presentation、Appの役割分担
  • Generic Host(汎用ホスト)とDI(依存関係の注入)を使ったWPF(Windows Presentation Foundation)アプリの起動、およびMVVM(Model-View-ViewModel)とcode-behindの境界
  • GPSの区間を地図へ描画し、セッションや写真の情報を編集・保存する流れ

BLE(Bluetooth Low Energy)の通信プロトコルやファームウェアの記録処理は対象外です。Windows 側で受け取った記録の流れに絞ります。次に、全体像を俯瞰します🔍

データフローとアーキテクチャ

CoreS3 からの転送と PC(パーソナルコンピューター)上のフォルダーでは入口が異なりますが、取り込み後は同じアーカイブを使います。まず、取り込みから画面表示までの流れを示します。

ここでいうアーカイブは、取り込んだセッションを保存し、必要なときに読み出すアプリ側の領域です。圧縮ファイル形式を指すものではありません。

編集と写真添付は、画面からApplicationのユースケースを経由して保存します。

図の矢印は、それぞれ別の操作経路を表しています。次に、この流れを支える各プロジェクトの責務を確認します🧩

プロジェクトごとの責務

レイヤー 主な責務 参照ソース
🧭 Domain/Core WalkSession、TrackPoint、写真・地名のモデルと、区間・距離の分析 Models.cs
🔄 Application アーカイブ取り込み、BLE転送、編集、写真添付などのユースケースを調整 ArchiveImportUseCase.cs、WorkspaceUseCases.cs
🪟 Infrastructure.Windows Windows設定ストア、BLE転送ファクトリー、写真から EXIF(Exchangeable Image File Format)の撮影時刻を読み取る実装など WindowsAdapters.cs
🎛️ Presentation MainWindowViewModelが画面状態、コマンド、通知を扱う MainWindowViewModel.cs
🖥️ App WPFのWindow、WebView2の地図、HostとDIの組み立て Program.cs、MainWindow.xaml

Models.csの名前空間はWalkLogger.Coreです。WalkSessionが点や写真を持ち、TrackPoint.Validate()がGPS点の妥当性を検査します。WalkAnalysisは接続区間と距離を集計し、Applicationがモデルを操作、Presentationが画面状態として公開します。

役割は分かれていますが、WPFの画面イベントまでViewModelへ移した純粋なMVVMというわけではありません。次はアプリの起動と、その境界を具体的に見ます🏗️

WPF Host / DIとMVVMの境界

2026年10月6日時点で確認した GitHub main では、アプリは net10.0-windows10.0.19041.0 を対象とします。WPF と Microsoft.Extensions.Hosting 10.0.0、Microsoft.Web.WebView2 1.0.3537.50 を使います。設定はプロジェクトファイルにあります。

Generic Host はホストの開始・停止をまとめる枠組みです。DI(依存関係の注入)は、サービスの実装を登録し、利用側へ渡す仕組みです。Program.csがHostを構築し、Windows依存の実装、ユースケース、ViewModel、WindowをDI登録します。ここがcomposition root(依存関係の組み立て場所)です。App.xaml.csはStartupでHostを開始し、DIから解決したMainWindowを表示します。終了時はHostを停止・破棄します。

MainWindow.xaml.csではViewModelをDataContextに設定します。MainWindow.xamlには{Binding Walks}や{Binding Title}などがあり、ViewModelの状態を画面に結び付けています。状態・コマンド・通知はViewModelが担いますが、一覧選択(WalkList_SelectionChanged)やPasswordBox入力(ApiKeyBox_PasswordChanged)はcode-behindに残っています。

WalkLoggerはMVVMを採り入れていますが、画面イベントもcode-behindで処理するため、純粋なMVVMとは説明しません。地図もWebView2とRouteMapPresenterが担当します。次はインポートを追います🔧

記録を受け取り、セッションへ取り込む

ArchiveImportUseCase.ImportDirectoryAsyncは指定フォルダーからtrack.ndjsonを探します。見つからない場合は GPX(GPS Exchange Format)ファイルを検索し、該当ファイルをパス順にarchive.ImportAsyncへ渡します。対象がなければエラーです。検索の実装はFileAdapters.csにもあります。

CoreS3 からの転送では、BleTransferUseCaseがデバイスのcatalog(ファイル一覧)を取得し、ファイルをローカルのImports配下へダウンロードします。その後、同じArchiveImportUseCaseへ渡します。実装はWorkspaceUseCases.csとArchiveImportUseCase.csで確認できます。

取り込み後、MainWindowViewModelは返されたセッション ID(識別子)を使ってRefreshAsyncを呼び、アーカイブのLoadAsyncから記録一覧を読み直します。選択した記録をCurrentに反映し、ShowSessionから地図状態も更新します。つまり、インポート用ユースケースの戻り値を使って画面状態を再構成する流れです。

実際のファイル読み込みはTrackImporter.ReadAsyncが担います。拡張子が.gpxならGPX、それ以外ならNDJSONを1行ずつTrackPointへデシリアライズします。

var points = Path.GetExtension(path).Equals(".gpx", StringComparison.OrdinalIgnoreCase)
    ? ReadGpx(path)
    : await ReadJsonLinesAsync<TrackPoint>(path, ct);

読み込んだ各点はValidate()で座標・時刻・速度などを検証し、時刻が重複または逆行していないことも確認します。CoreS3のNDJSONにあるUTC時刻はDateTimeOffset型のTrackPoint.Timeに読み込まれ、緯度・経度・速度・高度に加えてsegment(記録区間番号)も保持されます。ArchiveStore.ImportAsyncは取り込み元のハッシュで重複を確認し、新しい記録をWalkSessionとしてwalk.jsonなどのアーカイブファイルへ保存します。これでGPS時刻と区間情報が、表示・分析に使うモデルへ引き継がれます。次は地図表示です🗺️

WebView2の地図へルートを渡す

WPFのWebView2とRouteMapPresenterが地図の橋渡しをします。Presenterはアプリ内のMapフォルダーを仮想ホストへ割り当て、https://walklogger.local/index.htmlを表示します。HTML(HyperText Markup Language)とJavaScriptはMap/index.html、Map/map.jsです。

PresenterはViewModelの地図状態から現在・前回ルート、写真、地名をJSON(JavaScript Object Notation)でWebView2へ渡します。現在ルートはWalkAnalysis.Segmentsで分割し、JavaScript側で別レイヤーと各種マーカーを描画します。写真の添付と時刻照合は、後の「セッションと写真の編集」で説明します。

背景タイルは必要時にtile.openstreetmap.orgから取得します。取得できなくてもエラーを知らせたうえでGPSルートを表示できます。

地図へ渡す区間の作り方は、距離集計にも関わります。次に、線をつなぐ条件を確認します📏

区間判定と距離の考え方

WalkAnalysis.Connectedは、隣り合う点のsegmentが同じで、時刻が前進し、点間の時間差が60秒以内、座標間距離から求めた推定速度が12 m/s以下の場合に接続します。

public static bool Connected(TrackPoint a, TrackPoint b) =>
    a.Segment == b.Segment && b.Time > a.Time && (b.Time - a.Time).TotalSeconds <= 60 &&
    DistanceMeters(a.Lat, a.Lon, b.Lat, b.Lon) / (b.Time - a.Time).TotalSeconds <= 12;

Segmentsは判定がfalseになった地点で新しい配列を開始し、距離集計も接続された点の組だけを加算します。segmentが変わった区間や、時間・速度条件を外れた区間は線でつながず、距離にも含めません。

この速度はTrackPoint.Speedではなく、座標間距離を時刻差で割った値です。地図と距離集計が同じ接続ルールを使います。

線の意味が分かったところで、画面から入力した情報をどう保存するかへ進みます📝

セッションと写真の編集

MainWindowViewModel.SaveCurrentAsyncは画面のタイトルやノートなどからSessionEditsを作り、SessionEditingUseCase.SaveAsyncへ渡します。空白だけのタイトルも拒否し、有効なタイトルは前後の空白を取り除いて保存します。ノート、ブログ本文、一周確認、写真メモもWalkSessionへ反映し、アーカイブストアに保存します。記録を切り替えるときも、ViewModelは現在の記録を保存してから次の記録を表示します。

MainWindowViewModel.AddPhotoAsyncは Windows 側のPhotoMetadataReaderでEXIFの撮影時刻を読み、PhotoAttachmentUseCaseに渡します。最も近い GPS 点が撮影時刻の60秒以内にある場合だけ添付し、記録を保存して画面を更新します。実装はMainWindowViewModel.cs、WorkspaceUseCases.cs、WindowsAdapters.csを参照してください。

時間が離れた写真を無条件に貼り付けない設計です。最後に、今回見た責務のつながりをまとめます✅

まとめ

WalkLogger の Windows 側では、ファイル転送と取り込み、ドメインモデル、画面状態、Windows 固有実装、WPF 表示を役割ごとに分けています。BLE 転送後もフォルダーからの取り込みと同じユースケースを通り、WalkSessionがViewModelと地図表示へつながります。

地図はRouteMapPresenterがWebView2へ状態を渡し、区間判定に沿って描画します。編集はApplicationのユースケースで保存されます。MVVMを採り入れていますが、一覧選択やPasswordBoxのイベントはcode-behindに残ります。

CoreS3 側はファームウェア編(Qiita)、Windows 側はMainWindowViewModel.cs、RouteMapPresenter.cs、Models.csをご覧ください。画面だけでなく、データがどの境界を越えるかも辿ってみてください🌱

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?