近くの店舗が突然、数万キロメートルも離れた場所に表示されても、位置情報サービスの不具合とは限りません。緯度と経度の取り違え、180度子午線をまたぐ座標の単純な引き算、範囲判定より先に行う距離の四捨五入でも、同じような問題が起こります。GNUMac のクラウド Mac では、まず固定座標で計算処理を検証し、その後で位置情報の入力とデバイスからのコールバックを接続できます。こうすれば、失敗したときに原因のある層を切り分けられ、地図を何度もドラッグして原因を探る必要がなくなります。
座標と距離の仕様を先に明確にする
この記事で検証するのは「2つの座標間の地表距離」です。車で移動する経路の長さでも、ユーザーが実際に歩いた距離でもありません。入力は緯度、経度の順で、十進法の度数を使います。内部では距離の単位をメートルに統一し、画面に表示するときだけ単位を変換します。
緯度は−90度から+90度、経度は−180度から+180度の範囲内である必要があり、有限数でない値も拒否します。不正な入力を黙って範囲の上限や下限に丸め込んではいけません。上流で壊れたデータが、一見正常なレコードに化けてしまうためです。
座標として有効であることと、位置情報として信頼できることは別です。緯度0度、経度0度は有効な位置なので、無条件に「位置未取得」と扱うことはできません。受け入れるかどうかは、データ提供元との仕様に基づいて決める必要があります。
地図上の画面上の距離も、期待値には使えません。投影法、ズーム、デバイスのピクセルによって見た目は変わりますが、業務ロジックの計算結果が変わってはいけません。
クラウド Mac で固定サンプルを実行する
使用する開発ツールチェーンを選択済みの macOS 環境を用意し、xcrun swift --version を実行してコンパイラが利用できることを確認します。次の内容を distance-check.swift として保存してください。このスクリプトは現在地を取得せず、位置情報の利用許可もネットワークアクセスも必要としません。
import Foundation
import CoreLocation
enum CoordinateError: Error {
case invalid
}
func point(_ latitude: Double, _ longitude: Double) throws -> CLLocation {
let coordinate = CLLocationCoordinate2D(
latitude: latitude,
longitude: longitude
)
guard latitude.isFinite,
longitude.isFinite,
CLLocationCoordinate2DIsValid(coordinate) else {
throw CoordinateError.invalid
}
return CLLocation(latitude: latitude, longitude: longitude)
}
func rejects(_ latitude: Double, _ longitude: Double) -> Bool {
do {
_ = try point(latitude, longitude)
return false
} catch {
return true
}
}
let origin = try point(0, 0)
precondition(origin.distance(from: origin) == 0)
let east = try point(0, 179.9)
let west = try point(0, -179.9)
let crossing = east.distance(from: west)
precondition(crossing > 20_000 && crossing < 23_000)
precondition(abs(crossing - west.distance(from: east)) < 1)
let polarA = try point(89.9, 0)
let polarB = try point(89.9, 180)
let polarDistance = polarA.distance(from: polarB)
precondition(polarDistance > 20_000 && polarDistance < 23_000)
precondition(rejects(91, 0))
precondition(rejects(0, 181))
precondition(rejects(.nan, 0))
precondition(rejects(0, .infinity))
実行コマンドは次のとおりです。すべてのアサーションを通過すれば出力はなく、失敗すると非ゼロの終了ステータスを返すため、そのままパイプラインのステップとして使えます。
xcrun swift distance-check.swift
ここで指定した範囲は、テストフィクスチャに設けた広めの許容範囲です。性能の実測値でも、業務上の精度を保証するものでもありません。180度子午線を挟んだ両側の点は近くにあります。また、極域では経度差が大きくても、地球を半周するほど離れているわけではありません。この2つのケースは、緯度と経度を平面座標のように単純に引き算する誤りを検出できます。
ジオフェンスの境界判定を純粋関数として切り出す
業務ルールを「距離が半径以下なら内側と判定する」と仮定します。まず人工的に与えた距離で比較処理をテストし、その後で座標から計算した距離を渡します。2つの層を同時にデバッグしないためです。次のコードを同じファイルの末尾に追加して、もう一度実行してください。
func isInside(distance: Double, radius: Double) -> Bool {
guard distance.isFinite, radius.isFinite,
distance >= 0, radius >= 0 else {
return false
}
return distance <= radius
}
precondition(isInside(distance: 499.9, radius: 500))
precondition(isInside(distance: 500, radius: 500))
precondition(!isInside(distance: 500.1, radius: 500))
precondition(!isInside(distance: .nan, radius: 500))
precondition(!isInside(distance: 0, radius: -1))
let meters = 1_250.0
precondition(meters / 1_000 == 1.25)
境界値のサンプルには、明らかに内側、明らかに外側のケースだけでなく、ちょうど等しいケースも含めます。「0.5キロメートル」と表示された文字列から判定結果を逆算してはいけません。元の距離が異なっていても、丸めによって同じ表示になることがあるためです。
境界付近での切り替わりを抑えたい場合は、進入と退出で異なるしきい値を使うヒステリシスを定義できます。ただし、これは状態機械の問題なので、連続する入力列のテストが別途必要です。比較処理に「だいたいこの程度」という定数を安易に足すものではありません。
測位精度と計算の許容誤差を分ける
実際の位置情報サンプルには、時刻と水平精度も含まれます。入力を受け付ける層では、古くなったサンプルを明示的に拒否し、水平精度が負の値の場合も適切に扱う必要があります。座標としては有効でも精度が低い場合は、そのまま「進入確定」イベントを生成すべきではありません。
根拠を確認できる保守的なルールとして、距離を d、水平精度を a、半径を r とします。d+a≤r のときだけ確実に内側、d−a>r のときだけ確実に外側と判定し、それ以外は不確定とします。これは業務上の保守的な判断であり、精度の値を絶対的な誤差保証として解釈するものではありません。
テストでは、精度の高いサンプル、精度の低いサンプル、古いサンプル、無効なサンプルをそれぞれ注入します。不確定の状態が維持されるか、画面が誤って確定的な結果を表示しないかを確認してください。計算のアサーションを緩めて、測位のノイズを隠してはいけません。
検証記録を残し、iOS プロジェクトに組み込む
このスクリプトは、フレームワークの挙動を手早く再確認するのに適しています。本番の実装に組み込む際は、座標の検証処理と比較処理をテスト可能なモジュールに置き、テストターゲットから本番の実装を直接呼び出します。アルゴリズムは1つだけを保守し、スクリプトは正しいのにアプリでは古いロジックが動いている、という事態を防ぎます。
実行のたびに、コミット識別子、ツールチェーンのバージョン、フィクスチャの座標、メートル単位の生の値、失敗したアサーションを記録します。システムの更新後に失敗した場合は、入力検証、距離の範囲、状態判定ルールのどこで失敗したのかを先に確認し、すべての期待値をいきなり更新しないでください。
最後に、iOS 実機での受け入れ確認も1回は残しておきます。領域監視イベント、バックグラウンド動作、実際の移動は、別の処理経路に属します。純粋な計算テストの合格が示すのは、固定入力に対して仕様どおりの結果が得られたことだけです。この検証範囲の境界を受け入れ確認の記録に明記するほうが、ランダムな座標を増やすよりも有益です。
よくある質問
Mac のスクリプトが成功すれば iOS のジオフェンス検証は完了ですか?
いいえ。確認できるのは座標処理、距離計算、しきい値の判定規則です。領域イベント、バックグラウンド動作、実際の位置精度は iOS 実機で別途検証します。
表示されたキロメートル値で領域への進入を判定できますか?
できません。判定には丸めていないメートル値を使います。単位変換と丸めは表示層で行い、位置精度への対応は別の規則として定義します。
ワークフローに合わせて、専用の Mac mini を用意しましょう
プロジェクトに合わせて、モデル・ノード・利用期間をお選びください。ご注文前にメモリ、ストレージ、接続方法をご確認ください。実際の利用状況は、コンソールに表示される最新情報をご確認ください。