From a99c14dcc97215fb2bf0fdfa93f572e9b00c31d8 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 15:27:21 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E8=A8=BA=E6=96=AD=E6=83=85=E5=A0=B1?= =?UTF-8?q?=E3=81=AB=E5=B9=B3=E6=BB=91=E5=8C=96=E3=81=AE=E5=88=A4=E5=AE=9A?= =?UTF-8?q?=E6=9D=90=E6=96=99=E3=81=A8=E7=B5=90=E6=9E=9C=E3=82=92=E8=B6=B3?= =?UTF-8?q?=E3=81=97=E3=81=A6=E5=9C=B0=E4=B8=8B=E9=89=84=E5=88=86=E5=B2=90?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E3=81=88=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 持ち出していた accuracyHistory は DevOverlay のチャート用で、1秒ごとにサンプリングし 無効値へNaNを積む配列だった。一方 isAccuracyStable が見ているのは accuracyHistoryAtom で、 測位を受理するたびに積まれ無効値は捨てられる。両者は別物なので、地下鉄分岐 (skipSmoothing)に入っているかを持ち出した JSON から説明できなかった。 実際に自宅で取得したスナップショットの accuracyHistory が [null, 12.2, 12.2, 12.2] で、 NaN 由来の null が混ざることから気付いた。 filter として次の3つを足す。 - accuracyHistory: isAccuracyStable が見ている配列 - lineType: 地下鉄分岐の条件のうち路線側の入力 - skipSmoothing: setLocation が下した判定そのもの skipSmoothing は skipSmoothingAtom へ setLocation が記録する。同じ条件を DevOverlay 側で 組み直すと、判定と表示が別々に育って食い違うため、結果を読ませる。診断表示専用で パイプラインの判定には使わない。 チャート用の accuracyHistory も従来どおり残す。見た目の推移と判定材料は用途が違う。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Sfi8S4Yvob2sEzib4VUUBs --- src/components/DevOverlay.test.tsx | 18 ++++++++++ src/components/DevOverlay.tsx | 11 ++++++ src/store/atoms/location.test.ts | 43 ++++++++++++++++++++++++ src/store/atoms/location.ts | 11 ++++++ src/utils/devDiagnosticsSnapshot.test.ts | 19 +++++++++++ src/utils/devDiagnosticsSnapshot.ts | 22 +++++++++++- 6 files changed, 123 insertions(+), 1 deletion(-) diff --git a/src/components/DevOverlay.test.tsx b/src/components/DevOverlay.test.tsx index 79cc0bb34..4cd7fe160 100644 --- a/src/components/DevOverlay.test.tsx +++ b/src/components/DevOverlay.test.tsx @@ -8,11 +8,14 @@ import { BAD_ACCURACY_THRESHOLD } from '~/constants/threshold'; import * as remoteConfigModule from '~/lib/remoteConfig'; import { etaAnchorAtom } from '~/store/atoms/etaFallback'; import { + accuracyHistoryAtom, backgroundLocationTrackingAtom, locationAtom, rawLocationAtom, + skipSmoothingAtom, } from '~/store/atoms/location'; import { autoModeEnabledAtom } from '~/store/atoms/navigation'; +import { stationAtom } from '~/store/atoms/station'; import { isLEDThemeAtom } from '~/store/atoms/theme'; import { getEtaPhaseNow } from '~/utils/etaPhaseNow'; import DevOverlay, { @@ -106,6 +109,9 @@ describe('DevOverlay', () => { autoModeEnabled = false, etaPhase = null, etaAnchor = null, + filterAccuracyHistory = [15], + skipSmoothing = false, + station = null, }: { location?: unknown; rawLocation?: unknown; @@ -113,6 +119,9 @@ describe('DevOverlay', () => { autoModeEnabled?: boolean; etaPhase?: unknown; etaAnchor?: unknown; + filterAccuracyHistory?: number[]; + skipSmoothing?: boolean; + station?: unknown; } = {}) => { mockGetEtaPhaseNow.mockReturnValue(etaPhase as never); mockUseAtomValue.mockImplementation((atom) => { @@ -131,6 +140,15 @@ describe('DevOverlay', () => { if (atom === etaAnchorAtom) { return etaAnchor as never; } + if (atom === accuracyHistoryAtom) { + return filterAccuracyHistory as never; + } + if (atom === skipSmoothingAtom) { + return skipSmoothing as never; + } + if (atom === stationAtom) { + return station as never; + } if (atom === isLEDThemeAtom) { return false as never; } diff --git a/src/components/DevOverlay.tsx b/src/components/DevOverlay.tsx index 83072c378..d8ac17705 100644 --- a/src/components/DevOverlay.tsx +++ b/src/components/DevOverlay.tsx @@ -26,11 +26,14 @@ import { useTelemetryEnabled } from '~/hooks/useTelemetryEnabled'; import { getMaxPermitAccuracy, isEtaAssistEnabled } from '~/lib/remoteConfig'; import { etaAnchorAtom } from '~/store/atoms/etaFallback'; import { + accuracyHistoryAtom, backgroundLocationTrackingAtom, locationAtom, rawLocationAtom, + skipSmoothingAtom, } from '~/store/atoms/location'; import { autoModeEnabledAtom } from '~/store/atoms/navigation'; +import { stationAtom } from '~/store/atoms/station'; import { copyTextToClipboard } from '~/utils/clipboard'; import { formatDevDiagnosticsSnapshot } from '~/utils/devDiagnosticsSnapshot'; import { @@ -463,6 +466,11 @@ const DevOverlay: React.FC = ({ unrotated = false }) => { const isBackgroundLocationTracking = useAtomValue( backgroundLocationTrackingAtom ); + // 平滑化の要否を決めている履歴と、その判定結果。チャート用のchartHistoryとは + // 別物なので、診断の持ち出しでは両方を出す。 + const filterAccuracyHistory = useAtomValue(accuracyHistoryAtom); + const skipSmoothing = useAtomValue(skipSmoothingAtom); + const currentLineType = useAtomValue(stationAtom)?.line?.lineType ?? null; // ETA補助の診断表示。有効フラグ(リモート設定/手動トグル)は非リアクティブなgetter、 // アンカーはatomから購読する。推定フェーズは常駐タイマーで公開されなくなったため、 // DevOverlay自身の1秒ティック(nowTick)を評価時刻としてオンデマンド計算する。 @@ -555,6 +563,9 @@ const DevOverlay: React.FC = ({ unrotated = false }) => { rawLocation, filteredLocation: simulatedLocation, accuracyHistory: chartHistory, + filterAccuracyHistory, + skipSmoothing, + lineType: currentLineType, effectiveSpeedMps: effectiveSpeed, hasMeasuredSpeed: hasEverMeasuredSpeed, maxPermitAccuracy, diff --git a/src/store/atoms/location.test.ts b/src/store/atoms/location.test.ts index 2b4f905a5..9fad5df9e 100644 --- a/src/store/atoms/location.test.ts +++ b/src/store/atoms/location.test.ts @@ -9,6 +9,7 @@ import { resetLocationState, setLocation, setRawLocation, + skipSmoothingAtom, } from './location'; import stationState from './station'; @@ -118,6 +119,48 @@ describe('setLocation', () => { }); }); + describe('地下鉄分岐を通ったかの記録', () => { + // 診断の持ち出し(DevOverlay)がこの値を読む。同じ条件を外で組み直すと、 + // 判定と表示が別々に育って食い違うため、setLocationが下した結果そのものを固定する。 + it('地下鉄かつ精度が不安定なら真になる', () => { + setStationLineType(LineType.Subway); + store.set(accuracyHistoryAtom, [10, 300, 20, 400]); + + setLocation(makeLocation(35.0, 139.0, 500, 1000)); + + expect(store.get(skipSmoothingAtom)).toBe(true); + }); + + it('地上路線なら偽になる', () => { + setStationLineType(LineType.Normal); + store.set(accuracyHistoryAtom, [10, 300, 20, 400]); + + setLocation(makeLocation(35.0, 139.0, 500, 1000)); + + expect(store.get(skipSmoothingAtom)).toBe(false); + }); + + it('地下鉄でも精度履歴が安定していれば偽になる', () => { + setStationLineType(LineType.Subway); + store.set(accuracyHistoryAtom, [30, 35, 28, 32]); + + setLocation(makeLocation(35.0, 139.0, 30, 1000)); + + expect(store.get(skipSmoothingAtom)).toBe(false); + }); + + it('リセットで偽へ戻る', () => { + setStationLineType(LineType.Subway); + store.set(accuracyHistoryAtom, [10, 300, 20, 400]); + setLocation(makeLocation(35.0, 139.0, 500, 1000)); + expect(store.get(skipSmoothingAtom)).toBe(true); + + resetLocationState(); + + expect(store.get(skipSmoothingAtom)).toBe(false); + }); + }); + describe('地下鉄路線', () => { it('精度が不安定な場合はスムージングをスキップする', () => { setStationLineType(LineType.Subway); diff --git a/src/store/atoms/location.ts b/src/store/atoms/location.ts index 1edb9341c..cf9718221 100644 --- a/src/store/atoms/location.ts +++ b/src/store/atoms/location.ts @@ -106,6 +106,12 @@ export const backgroundLocationTrackingAtom = atom(false); // 下流の処理が「現在位置を信用できない=走行中」と扱えるようにする。 export const locationAccuracyOutlierAtom = atom(false); +// 直近のsetLocationが地下鉄分岐(平滑化スキップ)を通ったか。診断表示専用で、 +// パイプラインの判定には使わない。地下の挙動を調べるとき、locationAtomの値だけでは +// どちらの経路を通ったか分からず、条件(lineTypeと精度履歴)を外から組み直すと +// 判定と食い違うため、結果そのものを残す。 +export const skipSmoothingAtom = atom(false); + // EMAスムージングの基準として使う「最後にフィルタ処理を通過した位置」 // 地下鉄モード中は更新しないため、モード復帰後にノイジーなprevで誤棄却されるのを防ぐ const lastFilteredLocationAtom = atom(null); @@ -128,6 +134,7 @@ export const resetLocationState = () => { store.set(lastFilteredLocationAtom, null); store.set(lastRawLocationAtom, null); store.set(locationAccuracyOutlierAtom, false); + store.set(skipSmoothingAtom, false); consecutiveSpeedRejections = 0; resetEtaBoundHold(); }; @@ -261,6 +268,10 @@ export const setLocation = (location: Location.LocationObject) => { const currentLineType = store.get(stationState).station?.line?.lineType; const skipSmoothing = currentLineType === LineType.Subway && !isAccuracyStable(updatedHistory); + // 判定の結果そのものを残す。DevOverlayの診断表示から「いま地下鉄分岐に入って + // いるか」を読めるようにするためで、同じ条件を呼び出し側で組み直すと + // 判定と表示が別々に育って食い違う。 + store.set(skipSmoothingAtom, skipSmoothing); // ETAが許す進行量を超えた測位は、どちらの経路へも通さない if (isImplausibleByEta(location)) { diff --git a/src/utils/devDiagnosticsSnapshot.test.ts b/src/utils/devDiagnosticsSnapshot.test.ts index 9cd9ca299..b251ec54a 100644 --- a/src/utils/devDiagnosticsSnapshot.test.ts +++ b/src/utils/devDiagnosticsSnapshot.test.ts @@ -37,6 +37,9 @@ const baseInput: DevDiagnosticsInput = { rawLocation: makeLocation(35.732538, 139.670653, 312, 1_700_000_000_000), filteredLocation: makeLocation(35.7325, 139.6706, 312, 1_700_000_000_000), accuracyHistory: [20, 45, 310], + filterAccuracyHistory: [20, 45, 310, 620], + skipSmoothing: true, + lineType: 'Subway', effectiveSpeedMps: 12.5, hasMeasuredSpeed: true, maxPermitAccuracy: 1500, @@ -89,6 +92,22 @@ describe('buildDevDiagnosticsSnapshot', () => { expect(snapshot.location.accuracyHistory).toEqual([20, 45, 310]); }); + it('平滑化の判定材料と結果を持つ', () => { + // locationAtomの値だけでは、平滑化を掛けたのか生の座標を入れたのかが区別できない。 + // チャート用の精度履歴とは別に、判定に使われた履歴と結果を持ち出す + const snapshot = buildDevDiagnosticsSnapshot(baseInput); + + expect(snapshot.filter).toEqual({ + skipSmoothing: true, + lineType: 'Subway', + accuracyHistory: [20, 45, 310, 620], + }); + // チャート用とは別物であることを固定する(取り違えると地下鉄分岐の説明が付かない) + expect(snapshot.filter.accuracyHistory).not.toEqual( + snapshot.location.accuracyHistory + ); + }); + it('測位が無い場合もnullで表現して壊れない', () => { const snapshot = buildDevDiagnosticsSnapshot({ ...baseInput, diff --git a/src/utils/devDiagnosticsSnapshot.ts b/src/utils/devDiagnosticsSnapshot.ts index b1e3f64f5..d8c03c110 100644 --- a/src/utils/devDiagnosticsSnapshot.ts +++ b/src/utils/devDiagnosticsSnapshot.ts @@ -30,8 +30,21 @@ export type DevDiagnosticsInput = { rawLocation: Location.LocationObject | null; /** フィルタ・スムージングを通した、アプリが現在地として使っている値 */ filteredLocation: Location.LocationObject | null; - /** DevOverlay のチャートが持つ精度履歴(古い順) */ + /** + * DevOverlay のチャートが持つ精度履歴(古い順)。1秒ごとのサンプリングで、 + * 測位が無い間は NaN が積まれる(JSONでは null になる)。見た目の推移用。 + */ accuracyHistory: number[]; + /** + * 平滑化の要否(isAccuracyStable)を決めている accuracyHistoryAtom の中身。 + * 測位を受理するたびに積まれ、無効値は捨てられるのでチャート用とは別物。 + * 地下鉄分岐に入っているかを説明できるのはこちらなので、必ず一緒に持ち出す。 + */ + filterAccuracyHistory: number[]; + /** 直近の測位が地下鉄分岐(平滑化スキップ)を通ったか */ + skipSmoothing: boolean; + /** 地下鉄分岐の条件のうち、路線側の入力 */ + lineType: string | null; /** 表示に使っている速度(m/s)と、それが実測かどうか */ effectiveSpeedMps: number; hasMeasuredSpeed: boolean; @@ -112,6 +125,13 @@ export const buildDevDiagnosticsSnapshot = (input: DevDiagnosticsInput) => ({ // 変位から算出した値か、測位が運んできた実測かを区別する speedIsMeasured: input.hasMeasuredSpeed, }, + // どちらの経路を通ったかと、その判定材料。locationAtomの値だけでは + // 平滑化を掛けたのか生の座標を入れたのかが区別できない。 + filter: { + skipSmoothing: input.skipSmoothing, + lineType: input.lineType, + accuracyHistory: input.filterAccuracyHistory, + }, eta: { phase: input.etaPhase, anchor: input.etaAnchor, From 67f7a59d1d8b4793c4b680d44b81394be8946a2c Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 15 Sep 2026 15:37:45 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E5=B9=B3=E6=BB=91=E5=8C=96=E3=81=AE?= =?UTF-8?q?=E5=88=A4=E5=AE=9A=E7=B5=90=E6=9E=9C=E3=81=A8=E5=88=A4=E5=AE=9A?= =?UTF-8?q?=E3=81=AB=E4=BD=BF=E3=81=A3=E3=81=9F=E8=B7=AF=E7=B7=9A=E7=A8=AE?= =?UTF-8?q?=E5=88=A5=E3=82=92=E5=90=8C=E3=81=98atom=E3=81=B8=E3=81=BE?= =?UTF-8?q?=E3=81=A8=E3=82=81=E3=81=A6=E8=A8=BA=E6=96=AD=E3=81=AE=E9=A3=9F?= =?UTF-8?q?=E3=81=84=E9=81=95=E3=81=84=E3=82=92=E9=98=B2=E3=81=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Sfi8S4Yvob2sEzib4VUUBs --- src/components/DevOverlay.test.tsx | 37 ++++++++++------ src/components/DevOverlay.tsx | 13 +++--- src/store/atoms/location.test.ts | 67 ++++++++++++++++++++++++++--- src/store/atoms/location.ts | 47 +++++++++++++++----- src/utils/devDiagnosticsSnapshot.ts | 8 +++- 5 files changed, 135 insertions(+), 37 deletions(-) diff --git a/src/components/DevOverlay.test.tsx b/src/components/DevOverlay.test.tsx index 4cd7fe160..0496a3fc5 100644 --- a/src/components/DevOverlay.test.tsx +++ b/src/components/DevOverlay.test.tsx @@ -2,7 +2,7 @@ import { act, fireEvent, render } from '@testing-library/react-native'; import * as Application from 'expo-application'; import { useAtomValue } from 'jotai'; import { Dimensions, StyleSheet } from 'react-native'; -import type { Station } from '~/@types/graphql'; +import { LineType, type Station } from '~/@types/graphql'; import { MAX_PERMIT_ACCURACY } from '~/constants/location'; import { BAD_ACCURACY_THRESHOLD } from '~/constants/threshold'; import * as remoteConfigModule from '~/lib/remoteConfig'; @@ -12,10 +12,9 @@ import { backgroundLocationTrackingAtom, locationAtom, rawLocationAtom, - skipSmoothingAtom, + smoothingDecisionAtom, } from '~/store/atoms/location'; import { autoModeEnabledAtom } from '~/store/atoms/navigation'; -import { stationAtom } from '~/store/atoms/station'; import { isLEDThemeAtom } from '~/store/atoms/theme'; import { getEtaPhaseNow } from '~/utils/etaPhaseNow'; import DevOverlay, { @@ -110,8 +109,7 @@ describe('DevOverlay', () => { etaPhase = null, etaAnchor = null, filterAccuracyHistory = [15], - skipSmoothing = false, - station = null, + smoothingDecision = { skipSmoothing: false, lineType: null }, }: { location?: unknown; rawLocation?: unknown; @@ -120,8 +118,7 @@ describe('DevOverlay', () => { etaPhase?: unknown; etaAnchor?: unknown; filterAccuracyHistory?: number[]; - skipSmoothing?: boolean; - station?: unknown; + smoothingDecision?: { skipSmoothing: boolean; lineType: unknown }; } = {}) => { mockGetEtaPhaseNow.mockReturnValue(etaPhase as never); mockUseAtomValue.mockImplementation((atom) => { @@ -143,11 +140,8 @@ describe('DevOverlay', () => { if (atom === accuracyHistoryAtom) { return filterAccuracyHistory as never; } - if (atom === skipSmoothingAtom) { - return skipSmoothing as never; - } - if (atom === stationAtom) { - return station as never; + if (atom === smoothingDecisionAtom) { + return smoothingDecision as never; } if (atom === isLEDThemeAtom) { return false as never; @@ -348,6 +342,25 @@ describe('DevOverlay', () => { ); }); + // filterのskipSmoothingとlineTypeは、判定時に1つのatomへまとめて書かれた組を + // そのまま出す。片方をstationAtomから読み直すと、測位と無関係な路線の + // 切り替わりで持ち出し時の値だけが進み、両者で検算できなくなる。 + it('平滑化の判定は結果と入力を同じ組のまま出力する', async () => { + setupAtomValues({ + smoothingDecision: { skipSmoothing: true, lineType: LineType.Subway }, + }); + const { getByTestId } = render(); + + fireEvent.press(getByTestId('dev-overlay-copy-button')); + await act(async () => {}); + + const copied = JSON.parse(mockCopyTextToClipboard.mock.calls[0][0]); + expect(copied.filter).toMatchObject({ + skipSmoothing: true, + lineType: LineType.Subway, + }); + }); + it('押した直後はCOPIED表示になり、一定時間で戻る', async () => { jest.useFakeTimers(); try { diff --git a/src/components/DevOverlay.tsx b/src/components/DevOverlay.tsx index d8ac17705..5a6d78539 100644 --- a/src/components/DevOverlay.tsx +++ b/src/components/DevOverlay.tsx @@ -30,10 +30,9 @@ import { backgroundLocationTrackingAtom, locationAtom, rawLocationAtom, - skipSmoothingAtom, + smoothingDecisionAtom, } from '~/store/atoms/location'; import { autoModeEnabledAtom } from '~/store/atoms/navigation'; -import { stationAtom } from '~/store/atoms/station'; import { copyTextToClipboard } from '~/utils/clipboard'; import { formatDevDiagnosticsSnapshot } from '~/utils/devDiagnosticsSnapshot'; import { @@ -468,9 +467,11 @@ const DevOverlay: React.FC = ({ unrotated = false }) => { ); // 平滑化の要否を決めている履歴と、その判定結果。チャート用のchartHistoryとは // 別物なので、診断の持ち出しでは両方を出す。 + // 判定結果と、その判定に使ったlineTypeは同じatomから取る。lineTypeを + // stationAtomから読むと、測位と無関係な路線の切り替わりで持ち出し時の値だけが + // 進み、判定時のskipSmoothingと食い違う。 const filterAccuracyHistory = useAtomValue(accuracyHistoryAtom); - const skipSmoothing = useAtomValue(skipSmoothingAtom); - const currentLineType = useAtomValue(stationAtom)?.line?.lineType ?? null; + const smoothingDecision = useAtomValue(smoothingDecisionAtom); // ETA補助の診断表示。有効フラグ(リモート設定/手動トグル)は非リアクティブなgetter、 // アンカーはatomから購読する。推定フェーズは常駐タイマーで公開されなくなったため、 // DevOverlay自身の1秒ティック(nowTick)を評価時刻としてオンデマンド計算する。 @@ -564,8 +565,8 @@ const DevOverlay: React.FC = ({ unrotated = false }) => { filteredLocation: simulatedLocation, accuracyHistory: chartHistory, filterAccuracyHistory, - skipSmoothing, - lineType: currentLineType, + skipSmoothing: smoothingDecision.skipSmoothing, + lineType: smoothingDecision.lineType, effectiveSpeedMps: effectiveSpeed, hasMeasuredSpeed: hasEverMeasuredSpeed, maxPermitAccuracy, diff --git a/src/store/atoms/location.test.ts b/src/store/atoms/location.test.ts index 9fad5df9e..ea46b7cc6 100644 --- a/src/store/atoms/location.test.ts +++ b/src/store/atoms/location.test.ts @@ -9,7 +9,7 @@ import { resetLocationState, setLocation, setRawLocation, - skipSmoothingAtom, + smoothingDecisionAtom, } from './location'; import stationState from './station'; @@ -128,7 +128,10 @@ describe('setLocation', () => { setLocation(makeLocation(35.0, 139.0, 500, 1000)); - expect(store.get(skipSmoothingAtom)).toBe(true); + expect(store.get(smoothingDecisionAtom)).toEqual({ + skipSmoothing: true, + lineType: LineType.Subway, + }); }); it('地上路線なら偽になる', () => { @@ -137,7 +140,10 @@ describe('setLocation', () => { setLocation(makeLocation(35.0, 139.0, 500, 1000)); - expect(store.get(skipSmoothingAtom)).toBe(false); + expect(store.get(smoothingDecisionAtom)).toEqual({ + skipSmoothing: false, + lineType: LineType.Normal, + }); }); it('地下鉄でも精度履歴が安定していれば偽になる', () => { @@ -146,18 +152,65 @@ describe('setLocation', () => { setLocation(makeLocation(35.0, 139.0, 30, 1000)); - expect(store.get(skipSmoothingAtom)).toBe(false); + expect(store.get(smoothingDecisionAtom)).toEqual({ + skipSmoothing: false, + lineType: LineType.Subway, + }); + }); + + it('駅が無ければ路線種別はnullで記録する', () => { + setStationLineType(null); + store.set(accuracyHistoryAtom, [10, 300, 20, 400]); + + setLocation(makeLocation(35.0, 139.0, 500, 1000)); + + expect(store.get(smoothingDecisionAtom)).toEqual({ + skipSmoothing: false, + lineType: null, + }); }); - it('リセットで偽へ戻る', () => { + // 回帰: 結果と入力を別々のatom(あるいは片方をstationAtomの直読み)で持つと、 + // 測位と無関係な路線の切り替わりで入力側だけが進み、持ち出した診断の + // skipSmoothingとlineTypeが別の瞬間の値になって検算できなくなる。 + it('判定後に路線が変わっても判定時の組を保つ', () => { setStationLineType(LineType.Subway); store.set(accuracyHistoryAtom, [10, 300, 20, 400]); setLocation(makeLocation(35.0, 139.0, 500, 1000)); - expect(store.get(skipSmoothingAtom)).toBe(true); + + setStationLineType(LineType.Normal); + + expect(store.get(smoothingDecisionAtom)).toEqual({ + skipSmoothing: true, + lineType: LineType.Subway, + }); + }); + + // 判定が変わらない限り同じオブジェクトを保つ。測位のたびに新しい参照を入れると、 + // 購読しているDevOverlayが1秒ごとに再レンダーする。 + it('判定が変わらなければ参照を作り直さない', () => { + setStationLineType(LineType.Subway); + store.set(accuracyHistoryAtom, [10, 300, 20, 400]); + setLocation(makeLocation(35.0, 139.0, 500, 1000)); + const first = store.get(smoothingDecisionAtom); + + setLocation(makeLocation(35.0001, 139.0001, 500, 2000)); + + expect(store.get(smoothingDecisionAtom)).toBe(first); + }); + + it('リセットで初期値へ戻る', () => { + setStationLineType(LineType.Subway); + store.set(accuracyHistoryAtom, [10, 300, 20, 400]); + setLocation(makeLocation(35.0, 139.0, 500, 1000)); + expect(store.get(smoothingDecisionAtom).skipSmoothing).toBe(true); resetLocationState(); - expect(store.get(skipSmoothingAtom)).toBe(false); + expect(store.get(smoothingDecisionAtom)).toEqual({ + skipSmoothing: false, + lineType: null, + }); }); }); diff --git a/src/store/atoms/location.ts b/src/store/atoms/location.ts index cf9718221..04b6b210d 100644 --- a/src/store/atoms/location.ts +++ b/src/store/atoms/location.ts @@ -106,11 +106,27 @@ export const backgroundLocationTrackingAtom = atom(false); // 下流の処理が「現在位置を信用できない=走行中」と扱えるようにする。 export const locationAccuracyOutlierAtom = atom(false); -// 直近のsetLocationが地下鉄分岐(平滑化スキップ)を通ったか。診断表示専用で、 -// パイプラインの判定には使わない。地下の挙動を調べるとき、locationAtomの値だけでは -// どちらの経路を通ったか分からず、条件(lineTypeと精度履歴)を外から組み直すと -// 判定と食い違うため、結果そのものを残す。 -export const skipSmoothingAtom = atom(false); +// 直近のsetLocationが下した平滑化の判定。診断表示専用で、パイプラインの判定には使わない。 +// 地下の挙動を調べるとき、locationAtomの値だけではどちらの経路を通ったか分からず、 +// 条件を外から組み直すと判定と食い違うため、結果そのものを残す。 +// +// 結果(skipSmoothing)と入力(lineType)を1つのオブジェクトで持つ。別々のatomにすると、 +// lineTypeは測位と無関係に変わる(stationAtomの更新)ため、判定後に路線が変わった状態で +// 持ち出したときに「判定時のskipSmoothing」と「持ち出し時のlineType」という別の瞬間の +// 値が並び、両者で検算できなくなる。 +export type SmoothingDecision = { + skipSmoothing: boolean; + lineType: LineType | null; +}; + +const INITIAL_SMOOTHING_DECISION: SmoothingDecision = { + skipSmoothing: false, + lineType: null, +}; + +export const smoothingDecisionAtom = atom( + INITIAL_SMOOTHING_DECISION +); // EMAスムージングの基準として使う「最後にフィルタ処理を通過した位置」 // 地下鉄モード中は更新しないため、モード復帰後にノイジーなprevで誤棄却されるのを防ぐ @@ -134,7 +150,7 @@ export const resetLocationState = () => { store.set(lastFilteredLocationAtom, null); store.set(lastRawLocationAtom, null); store.set(locationAccuracyOutlierAtom, false); - store.set(skipSmoothingAtom, false); + store.set(smoothingDecisionAtom, INITIAL_SMOOTHING_DECISION); consecutiveSpeedRejections = 0; resetEtaBoundHold(); }; @@ -268,10 +284,21 @@ export const setLocation = (location: Location.LocationObject) => { const currentLineType = store.get(stationState).station?.line?.lineType; const skipSmoothing = currentLineType === LineType.Subway && !isAccuracyStable(updatedHistory); - // 判定の結果そのものを残す。DevOverlayの診断表示から「いま地下鉄分岐に入って - // いるか」を読めるようにするためで、同じ条件を呼び出し側で組み直すと - // 判定と表示が別々に育って食い違う。 - store.set(skipSmoothingAtom, skipSmoothing); + // 判定の結果と入力を、同じ瞬間の組として残す。DevOverlayの診断表示から + // 「いま地下鉄分岐に入っているか」と「その根拠」を読めるようにするためで、 + // 同じ条件を呼び出し側で組み直すと判定と表示が別々に育って食い違う。 + // 値が変わらないときは書かない。毎回新しいオブジェクトを入れると、購読側が + // 測位のたびに再レンダーする。 + const prevDecision = store.get(smoothingDecisionAtom); + if ( + prevDecision.skipSmoothing !== skipSmoothing || + prevDecision.lineType !== (currentLineType ?? null) + ) { + store.set(smoothingDecisionAtom, { + skipSmoothing, + lineType: currentLineType ?? null, + }); + } // ETAが許す進行量を超えた測位は、どちらの経路へも通さない if (isImplausibleByEta(location)) { diff --git a/src/utils/devDiagnosticsSnapshot.ts b/src/utils/devDiagnosticsSnapshot.ts index d8c03c110..296d73f59 100644 --- a/src/utils/devDiagnosticsSnapshot.ts +++ b/src/utils/devDiagnosticsSnapshot.ts @@ -41,9 +41,13 @@ export type DevDiagnosticsInput = { * 地下鉄分岐に入っているかを説明できるのはこちらなので、必ず一緒に持ち出す。 */ filterAccuracyHistory: number[]; - /** 直近の測位が地下鉄分岐(平滑化スキップ)を通ったか */ + /** + * 直近の測位が地下鉄分岐(平滑化スキップ)を通ったか。次の lineType と + * 対で受け取ること。どちらも smoothingDecisionAtom が同じ判定時に書いた値で、 + * 片方を stationAtom から読み直すと別の瞬間の値が混ざる。 + */ skipSmoothing: boolean; - /** 地下鉄分岐の条件のうち、路線側の入力 */ + /** 上の判定に使った路線種別(判定時の値) */ lineType: string | null; /** 表示に使っている速度(m/s)と、それが実測かどうか */ effectiveSpeedMps: number;