状態: Phase 0〜3実装済み(2026-09-07、track-core/app/pc-tool/tools-map green)。Phase 4(Tablet UI)・Phase 5(実機)は別実装単位
対象Issue: #131「通常の軌跡記録へ測位品質を保存し、Tablet/PCで色分け・除外できるようにする」
優先する正本:docs/standards/ui-design-guidelines.md、
docs/specifications/p6-trajectory-file-format.md、docs/specifications/p6-ui-spec.md、
docs/specifications/pc-trajectory-viewer.md
次の構成を第一案とする。
TRAJECTORY_POSITION_QUALITY(仮ID 2、version 1)で記録する。headerLength=50、recordLength=27とし、既存の14byteDtrkPositionSampleを各通常測位frameへ付ける。計画JSONとevent frameは持たせない。track-coreへ一本化する。TabletとPCは共通結果を描画するだけにする。UNKNOWN(品質記録なし)とし、品質を推測しない。V5へ上げる案より変更範囲が狭く、現在のV4 readerが持つ「未知schemaでも通常測位prefixを回収する」性質を
活用できる。旧アプリでも新ファイルの位置・時刻・COG・速度は読め、品質だけが見えない形でdowngrade互換を保てる。
また、現行DtrkV4.isWellFormed()の契約上、schema IDを持たない通常V4を単にrecordLength=27へ広げる案は
不正形式になる。品質suffixを追加するなら、ID 2を登録するかV5へ上げる必要があり、第一案は前者である。
2026-09-06の実走記録デフォルト_20260906-085610.dtrkをPC軌跡ビューアで確認すると、直進区間に
数m規模の蛇行が見えた。生NMEAと記録開始後0〜7分を照合した結果は次のとおり。
| 項目 | 結果 |
|---|---|
| RTK FIX(GGA品質4) | 41.8% |
| DGNSS(GGA品質2) | 53.4% |
| RTK FLOAT(GGA品質5) | 4.8% |
| 6:00〜6:30 | 約97%がDGNSS |
| 同区間のGST水平誤差推定 | 95%点で約2.69m |
| NTRIP通信断/再接続 | 3:50/3:55 |
蛇行はビューアが作った座標ではなく、低品質時のGGA位置としてDTRKへ保存されていた。一方、通常V4は
46/13で位置・COG・速度しか持たないため、保存後のファイル単体ではFIXとDGNSSを区別できない。
中心線が多数の短い線へ分断される問題は別に存在する。現行TrajectoryOffsetTransform.centerline()が
耕耘幅帯と同じ旋回除外を通り、上記ファイルでは16,137点中60.5%を除外し2,081区間へ分けていた。
この改善案は品質記録と品質フィルタを扱い、中心線の旋回除外責務は#124の後続判断として混ぜない。
GGA / RMC / GST
-> FixRepository
- TrackPoint(lat/lon/time/course/speed)
- DtrkPositionSample(fix quality/sat/HDOP/GST/age...)
-> TrajectoryRepository
-> 参照用軌跡: TrackPointだけを保持(HotだけfixQualityを別保持)
-> TrajectoryRecorder.record(point, sample)
-> 通常DtrkV4Writer: sampleを捨てて46/13
-> NAV debug writer: sampleを14byte suffixへ保存して27byte frame
基礎部品は既にある。FixRepositoryはGGA処理時にDtrkPositionSampleを作り、
TrajectoryRecorder.record(point, sample)まで渡している。欠落点は次の2箇所である。
DtrkV4Writer.startNew()がpositionSuffixSize=0でsampleを保存しない。TrackPointとCold参照履歴が品質を持たないため、ライブ表示へ全履歴の品質を渡せない。V4 offset 46の識別子は現行文書・コードでdebugFormatIdと呼んでいるが、通常品質記録にも使用するなら
wire上の役割は「extension schema識別子」と整理するのが自然である。既存IDの意味やbyte配置は変えない。
| extension ID | version | header | frame | 用途 |
|---|---|---|---|---|
| なし | — | 46byte | 13byte | 既存の通常V4。品質なし |
| 1 | 1 | 50byte + plan JSON | 27byte | NAVIGATION_FIELD_TEST_DEBUG。品質suffix + event |
| 2(案) | 1 | 50byte、payloadなし | 27byte | TRAJECTORY_POSITION_QUALITY。品質suffixのみ |
ID 2のheaderは、base 46byteにextensionFormatId UInt16とextensionFormatVersion UInt16を続けた
50byte固定とする。JSON payloadは0byteである。通常測位frameは既存の13byte prefixに既存14byte suffixを続ける。
負値markerをeventとして生成・解釈しない。
実装上の既存シンボル名は最小変更のため初弾では改名しない。通常用途を含むwire上の名称との対応は次のとおりとし、
コードコメントと正本仕様にも同じ対応表を置く。
| wire上の役割 | 初弾で据え置くコード名 |
|---|---|
| extension schema ID/version | debugFormatId/debugFormatVersion |
| extension header payload | debugHeaderPayload |
| record suffix | debugRecordSuffix等の既存名 |
DtrkV4Debug等の一括リネームは、通常品質記録の機能差分と混ぜず後続の整理候補とする。
既存DtrkPositionSampleをそのまま再利用する。
| 値 | 用途 |
|---|---|
ggaFixQuality |
GGAの生品質。0はsample未記録 |
numSatellites |
使用衛星数 |
hdop |
GGA HDOP |
gstHorizontalErrorM |
GST水平誤差 |
altitudeM |
GGA標高 |
rmcAgeMs |
点記録時のRMC鮮度 |
gstAgeMs |
点記録時のGST鮮度 |
GST値はgstAgeMsと一緒に解釈し、値だけを現在の誤差として扱わない。既存の5秒stale規則を表示に使う場合も、
記録値を変更せずreader後の分類・表示段階で判定する。
46/13をquality=nullとして読む。recordLengthに従ってframe境界を進め、13byte prefixだけを読む。suffixを品質と推測しない。nullとする。headerLength==50を必須にする。headerLength==50のpayloadが空ByteArrayになるため、ID 2ではreader結果をdebugHeaderPayload=nullへ正規化する。schema 1は従来どおり非空JSON payloadを必須とする。debugHeaderPayload != nullをJSON存在判定に使う既存consumerはschema 1判定を先に行う形へ揃え、現行readerも未知IDの通常測位prefixを回収できる。このため新しいID 2ファイルを旧アプリへ渡しても、品質は
利用できないが軌跡自体は表示できる。
現行DtrkV4WriterはsupportsEvents = positionSuffixSize == 14としている。ID 2も14byte suffixを持つと、
通常記録が意図せずevent対応になるため、この判定を残してはいけない。
supportsPositionQuality = extension ID が1または2
supportsEvents = extension ID が1のみ
recordEvent()、pending event、SESSION_FINALIZEDはID 1だけで有効とする。ID 2では常にno-opとする。
writerだけでなくTrajectoryRecordingCoordinator.beginNewFile()の分岐もschemaの用途で分ける。ID 2は通常記録なので
records/へ保存し、ID 1だけをrecords/debug/へ保存する。writer factoryはplain 46/13、通常品質ID 2、
NAV debug ID 1の3ブランチを明示し、debugHeaderPayload != nullを保存先・factory選択の代理条件にしない。
RecordingStarted/PlanConfirmedの発火もID 1だけに限定する。これにより、空payloadの表現に依存してID 2が
debug保存先やevent経路へ入ることを防ぐ。
V5なら名称を整理しやすい一方、version分岐、header reader、writer、全fixture、旧アプリの未知version時の挙動まで
変更になる。V4は元々headerLengthとrecordLengthを持つ拡張可能形式で、未知schemaのprefix回収も実装済みである。
さらにDtrkV4.isWellFormed()はrecordLength > 13 && headerLength < 50を不正とするため、schema IDなしの
46/27は仕様上選べない。新しい意味を既存IDへ再割当せずID 2を追加する方が、後方・downgrade互換と実装量の
バランスが良い。
ライブ表示、Cold圧縮後の表示、ファイル再生を同じAPIで扱うため、TrackPointがnullableな品質snapshotを持つ案を
採用する。ただしmodel packageからtrack packageへ逆依存させない。
dev.drogger.tablet.model.TrajectoryPointQualityを追加する。TrackPoint末尾へquality: TrajectoryPointQuality? = nullを追加する。DtrkPositionSampleはwire DTOとして残し、相互変換をtrack-coreに置く。FixRepositoryは同じGGA snapshotからTrackPoint.qualityとwriter用sampleを作る。DtrkFileReaderはID 1/2のsampleを復元し、対応するTrackPoint.qualityにも設定する。TrajectoryReferenceHistoryのarchiveはTrackPoint自体を保持するため、RDPで採用された点の品質も追加配線なしでTrajectoryReferenceHistory.restore()が復元点へ設定するHotEntry.fixQuality=0は、入口検出の品質gate用のTrackPoint.qualityとは別の責務とする。ファイルから復元した表示品質をこの値へ流用せず、これにより位置列と品質列のparallel listが描画経路でずれることを防ぐ。既存constructorは末尾default値により
source互換を保つ。
記録軌跡の分類はGGA生品質を一次判定にする。
| GGA値 | 表示区分 | 表示文言案 |
|---|---|---|
| 4 | FIX |
固定解 |
| 5 | FLOAT |
浮動解 |
| 2 | DGNSS |
差分測位 |
| 1 | SINGLE |
単独測位 |
| その他の正値 | OTHER |
その他の測位 |
| sampleなし/0 | UNKNOWN |
品質記録なし |
GGA品質0の無効fixは現在TrackPointを生成しないため、記録済みの点をINVALIDに分類しない。無効期間は点の欠落として
現れ、前後を接続しない規則で可視化する。STALEも受信後の現在時刻に依存するライブ状態であり、保存点の恒久分類には
入れない。
GST誤差は保存するが、初弾ではGGA品質区分を上書きする分類条件にしない。FIXかつGST誤差が大きい実例を集めた後、
独立した誤差閾値filterが必要か判断する。これにより「FIXの意味」と製品独自閾値を混同しない。
track-coreに純粋関数TrajectoryQualityRuns.partition(points, filter)相当を置く。run境界は次のいずれかで作る。
segmentIdが変わった。非表示点を単にリストから取り除いてから線を作ると、その前後を直線で橋渡ししてしまうため禁止する。
#124で確定した責務と旋回判定の入力系列を変えないため、処理順は次で固定する。
TrajectoryRenderGroupingで元のrecordedSegmentIdごとに分け、記録時offset・作業機幅を対応づける。centerline()、帯はapply(offsets)を従来どおり適用する。renderGroupId採番はこの段階で行う。renderGroupId、品質区分、filterによる非表示gapで品質runへ分ける。(qualityRunId, renderGroupId)相当とし、品質run IDを永続segmentIdや既存のrenderGroupIdへ上書きしない。中心線と帯は同じ元点列・同じ順序から品質runを作る。これにより品質境界で旋回判定のCOG履歴をリセットせず、#124の「中心線=apply(0,0)」「中心線と帯で旋回区間・
記録境界を共有」を維持できる。全品質表示時の中心線・帯geometryと分断位置が変更前と一致する回帰test、
品質非表示時だけ追加のgapが入るtest、異なる品質runの同じローカルrenderGroupIdをflattenしても接続しないtestを
Phase 2の必須gateとする。
filterはSet<TrajectoryQualityClass>として表し、複数Booleanを画面状態の正本にしない。
推奨初期値は次の2案を実機で比較する。
| 対象 | 案A(安全側、推奨) | 案B(現行互換) |
|---|---|---|
| アンテナ中心線 | 全区分を表示・色分け | 全区分を表示・色分け |
| 耕耘幅帯 | FIX + UNKNOWN | 全区分 |
案Aでは信頼できない位置を「耕耘済み範囲」として見せず、中心線には残して測位低下そのものを隠さない。
UNKNOWNを既定表示に含めるのは、旧ファイルの帯が更新後に突然消える後方互換問題を避けるためである。
既存PositionQualityBadgeと意味を揃え、色定数を共通の意味付きpaletteへ移す。
| 区分 | 色案 | 補助表現 |
|---|---|---|
| FIX | 緑 | 「固定解」 |
| FLOAT | 黄 | 「浮動解」 |
| DGNSS | 橙 | 「差分測位」 |
| SINGLE | 赤 | 「単独測位」 |
| OTHER | 青または紫 | 「その他の測位」 |
| UNKNOWN | 灰 | 「品質記録なし」 |
hex値は実装前にDroggerTheme.ktの意味付きtokenとして確定する。航空写真上で線幅3dp相当、帯opacity、選択中の筆、
計画線と同時表示して識別できるかを確認する。色だけに頼らず、表示設定内の色見本+文言と、再生画面の凡例を併用する。
MapDisplayOptionsSheetへ「軌跡の測位品質」を追加する。TrajectoryQualityDisplaySettingsとして保持し、固定解 42% / その他 58%のような短い品質要約を文言で表示する。品質記録なしと表示し、0% FIXとは表現しない。pc-tool GeoJSON featureへproperties.qualityを追加する。/api/trajectoryへ選択品質を明示するqueryを追加し、serverはallowlist検証後にpc-toolへ渡す。46/13を自動変換しない。UNKNOWNとして表示し、既定で中心線・帯とも表示する。各フェーズ末尾で既存suiteをgreenに保つ。形式を変えた後の既存テスト修正を後段へ先送りしない。
50/27、品質区分、初期filter案を利用者レビューする。p6-trajectory-file-format.md、p6-ui-spec.md、pc-trajectory-viewer.mdへ反映する。完了gate: Issue #131に採用案、却下案、既定表示、実機確認条件が記録されている。
TrajectoryPointQualityとTrackPoint.qualityを追加する。startNew()を品質付きへ切り替える。records/へ保存する。TrackPoint.qualityへ復元する。FixRepository、TrajectoryRepository、復元経路を品質付きにする。完了gate:
:track-core:test:app:testDebugUnitTest:pc-tool:testassembleReleaseTrajectoryQualityClassを得る純粋関数を追加する。完了gate: :track-core:testと既存のoffset/swath/render grouping回帰test。
pc-toolへ品質property・集計・filter引数を追加する。server.pyのquery allowlistとmap.js/HTMLの品質filter・凡例を追加する。完了gate:
:pc-tool:testtools/map testOfflineMapStyleを品質propertyに応じた線・帯描画へ変更する。MapDisplayOptionsSheetを拡張し、ライブと保存再生で共通のfilter状態を使う。PositionQualityBadgeも同じpaletteを参照する。完了gate:
:app:testDebugUnitTest:track-core:testassembleRelease50/27と品質復元をPCで確認する。完了gate: Issue #131の完了条件を実機証拠付きでチェックし、正本文書を実装結果へ更新する。
| 層 | 主なファイル |
|---|---|
| domain/format | model/TrackPoint.kt、track/DtrkPositionSample.kt、DtrkV4.kt、DtrkV4Writer.kt、DtrkFileReader.kt |
| 共通判定 | track/TrajectoryQuality*.kt(新規)、TrajectoryRenderGrouping.kt、TrajectoryOffsetTransform.kt |
| 受信・保持 | data/FixRepository.kt、track/TrajectoryRepository.kt、TrajectoryReferenceHistory.kt |
| Tablet描画 | ui/OfflineMapStyle.kt、MapScreen.kt、MapUtilityControls.kt、TrajectoryReplayScreen.kt、TrajectoryThumbnail.kt、TrajectoryListScreen.kt |
| PC変換 | pc-tool/Main.kt、GeoJsonWriter.kt |
| PC Web | tools/map/server.py、static/index.html、static/map.js、static/map-config.json |
| 正本文書 | 経路記録データファイルフォーマット.html、p6-ui-spec.md、pc-trajectory-viewer.md |
| リスク | 対策 |
|---|---|
| ID 2でもeventを書いてしまう | supportsEventsをsuffix長ではなくschema ID 1で判定し、負値frame不在をbyte testする |
| ID 2をdebug保存先へ置いてしまう | coordinatorの保存先・factory・event発火をschema用途で分岐し、ID 2がrecords/へ入るtestを書く |
| 空payloadを計画JSONとして読む | ID 2の空payloadをnullへ正規化し、JSON consumerをschema 1でgateする |
| 品質filter後に離れた点を直結する | 点削除ではなくrun分割を共通APIにし、gap testを必須にする |
| ライブだけ品質が残り、再起動後に消える | TrackPoint.qualityとDTRK reader復元を同Phaseで実装する |
| Cold圧縮で品質境界が消える | 品質runごとに圧縮するか、品質変化点を必須保持点にする |
| 旧ファイルの帯が消える | UNKNOWNを別区分とし既定表示する |
| 色が航空写真・計画線に埋もれる | 意味付きpalette、凡例、線幅、opacityを実機で同時表示確認する |
| filterが記録データを削除したと誤解される | UI文言を「表示する品質」とし、保存内容は変わらない旨を表示する |
| GSTの古い値を現在値扱いする | gstAgeMsを必須で併用し、stale時は誤差値を分類に使わない |
| 通常ファイル容量が約2倍になる | 13→27byte/frame(約2.08倍)を形式仕様へ注記する。9.5Hz×8時間ならheaderを除き約3.5MB→7.4MBで、現状は運用上許容範囲 |
Issue #131コメント6121では、次の組み合わせが推奨された。これをPhase 0の承認候補とする。
50/27を採用し、V5は新設しない。このレビューは設計案への推奨であり、Phase 0の正式承認として扱うまでは正本仕様やwriterの既定形式を変更しない。