移動経路表示機能を組み込んでいるアプリを作っているときに、CoreLocationから取得できる位置情報をそのまま使うと、最高精度にしてもあまり綺麗な移動軌跡が表示できずに困っていました。
適切な位置情報の取得設定、取得できた位置情報をフィルター処理を工夫すると、綺麗な移動軌跡が取れやすくなるので、ちょっとしたコツとしてまとめました。
サンプルコードは、いずれも以下のプロパティを持ったクラス内で実装している前提で記載しています。
// CLLocationManagerのインスタンス
@property (nonatomic) CLLocationManager *locationManager;
// 最後に取得した有効な位置情報
@property (nonatomic) CLLocation *latestLocation;
1. CLLocationManagerの取得設定を適切にする
アプリが必要な精度に応じて、GPSの取得条件の設定を行います。
正しく設定しないと、不用意にバッテリー電力を消費したり、必要な精度で位置情報が取れなくなったりするので、適切な設定を行うことが必須です。
特に注意する必要がありそうな設定をピックアップしました。
プロパティ | 概要 | 注意事項 |
---|---|---|
desiredAccuracy | 取得精度の設定 | 不用意に最高精度や10mにすると、バッテリーがすごい速さで無くなります。 |
distanceFilter | 位置情報取得間隔 | 小さい値を設定すると、一箇所にとどまっている時にも、位置情報が通知され続けます。3-5m以上を設定すると使い勝手が良いイメージ。この数値を小さくしても、位置情報の精度は上がらないので、要注意です! |
pausesLocationUpdatesAutomatically | activityTypeの設定に応じてOSが自動でGPSのON/OFFを制御 | YESにすると、ユーザーが一点から動作しなくなった時に、OSがGPSをOFFにしてバッテリーを節約してくれます。ただし、一旦GPSがOFFになるとアプリがフォアグランドに戻るまで、位置情報を取得できなくなります。 |
[例1] 位置情報を細かく取得するランニングアプリなど
// 測位の精度を指定(最高精度)
self.locationManager.desiredAccuracy = kCLLocationAccuracyBest;
// 位置情報取得間隔を指定(5m移動したら、位置情報を通知)
self.locationManager.distanceFilter = 5;
// 自動で位置情報取得がOFFになるのを防ぐ
// ※YESの場合は、activityTypeの設定に応じてOSが自動でGPSのON/OFFを制御する
self.locationManager.pausesLocationUpdatesAutomatically = NO;
// バックグランドでも位置情報を取得
self.locationManager.allowsBackgroundLocationUpdates = YES;
[例2] 常時移動ログを取得するアプリなど
// 測位の精度を指定(約100mごと。100m以下の移動でも、位置情報が通知されることが多々ある)
self.locationManager.desiredAccuracy = kCLLocationAccuracyHundredMeters;
// 位置情報取得間隔を指定(10m移動したら、位置情報を通知)
self.locationManager.distanceFilter = 10;
// 自動で位置情報取得がOFFになるのを防ぐ
self.locationManager.pausesLocationUpdatesAutomatically = NO;
// バックグランドでも位置情報を取得
self.locationManager.allowsBackgroundLocationUpdates = YES;
2. 信頼度の低い位置情報を取り除く
CLLocation.horizontalAccuracyで、取得した位置情報の精度を取得できます。
- 取得した位置情報が半径何mの精度があるか取得できる
- 精度が負の値の場合は垂直データのみのため、データを破棄する
- 室内の最高精度は、約60mほどになる模様
CLLocation *location = locations.lastObject;
if (location.horizontalAccuracy < 0) {
// 座標が無効な位置情報データなので破棄
return NO;
}
// 垂直精度が半径100m以下のみ、有効な位置情報として扱う
if (location.horizontalAccuracy <= 100) {
// 有効な精度を持った位置情報
return YES;
}
3. キャッシュされた位置情報を取り除く(計測中の古い位置情報を破棄)
計測した位置情報は、計測した時刻を取得することができます。
古いデータの場合、ランニングアプリなどでは不要なことが多いので、必要に応じて破棄します。
CLLocation *location = locations.lastObject;
double duration = location.timestamp.timeIntervalSinceNow;
if (duration >= 7) {
// 7秒以上経過したデータは破棄する
}
4. 計測開始した直後のデータを取り除く
CLLocationManagerのstartUpdatingLocation()を呼びだした直後の位置情報のデータは、不正確な位置情報が多く入っていることがあります。
特に、**現在の時刻のタイムスタンプで過去のキャッシュされた位置情報が送られてくる場合があった!**ので注意が必要でした。
(例えば、現在時刻のタイムスタンプで5分前の位置情報がきたり。。。)
したがって、計測開始時にLocationManagerのstartUpdatingLocationを呼び出す作りにしている場合は、最初の5-10秒間に取得できた位置情報は破棄してしまうのがオススメです。
破棄しない場合は、startUpdatingLocationを呼び出す前の位置情報が取得できてしまい、移動経路を表示するアプリでは、変な移動経路になってしまうことがあります。
5. 誤差の範囲内で移動したデータを取り除く
CLLocationManagerで最高精度で位置情報を取得している場合、動いていなくても、違う位置の位置情報が通知されます。
取得した位置情報をそのまま全て保存すると、一箇所にとどまっている時も誤差の範囲内でブレている移動軌跡になってしまいがちです。
そこで、前回取得した位置情報から一定距離離内の移動データ or 取得精度内の移動データを取り除くと、綺麗なデータが取れるようになります。
distanceFilterで、特定距離位置情報が離れた時のみ位置情報更新を通知するように設定
// 位置情報取得間隔を指定(10m移動したら、位置情報を通知)
self.locatinManager.distanceFilter = 10;
緯度/経度から移動した距離を計算してフィルター
最新の位置情報を取得しつつ、移動距離でフィルターしたい場合はこちら。
// 前回取得できた位置情報から10m以上離れていたら有効な位置情報として判定
double distance = fabs([self.latestLocation distanceFromLocation:location]);
if (distance >= 10) {
// 有効な位置情報
self.latestLocation = location;
} else {
// 無効な位置情報
}
まとめ
1から5の処理を移動ルート表示処理を含めたサンプルソースをViewController上に全てまとめてみました。
なお、簡単な正常系の処理のみの実装となります。
サンプルJP作成手順
- Xcodeでプロジェクト作成
- Info.plistの「NSLocationWhenInUseUsageDescription」を設定
- Info.plistの「NSLocationAlwaysAndWhenInUseUsageDescription」を設定
- Project -> Target -> Capabilites -> Backgroud Modes -> ON
- Project -> Target -> Capabilites -> Backgroud Modes -> Location updatesをチェック
- MKMapView、開始ボタン、停止ボタンをStoryboardに貼り付け
- ViewControllerとひもづける
サンプルのコード
/**
移動経路をMapViewに描画する サンプルViewController
*/
@interface ViewController : UIViewController
@end
/**
有効な位置情報か判定
*/
- (BOOL)isEnableLocation:(CLLocation *)location {
// 位置情報が入っていない場合は無効
if (location == nil) {
NSLog(@"位置情報が入っていない場合は無効");
return NO;
}
// 水平精度が0以下の位置情報データの場合は無効
if (location.horizontalAccuracy < 0) {
NSLog(@"水平精度が0以下の位置情報データの場合は無効");
return NO;
}
// 垂直精度が半径100m以上のデータの場合は無効
if (location.horizontalAccuracy > 100) {
NSLog(@"垂直精度が半径100m以上のデータの場合は無効");
return NO;
}
// 現在時刻から5秒以上前の計測データの場合は無効
double duration = location.timestamp.timeIntervalSinceNow;
if (duration >= 5) {
NSLog(@"現在時刻から5秒以上前の計測データの場合は無効");
return NO;
}
// 計測開始時間から、10秒以内の場合は無効
double diffStartTime = location.timestamp.timeIntervalSince1970 - self.startMeasurementDate.timeIntervalSince1970;
if (diffStartTime < 10) {
NSLog(@"計測開始時間から、10秒以内の場合は無効");
return NO;
}
// 初回データの場合は、この時点で有効データと判定
if (self.latestLocation == nil) {
// 初回取得データならば、この時点で有効とする
NSLog(@"有効データ");
return YES;
}
// ここからは、前回取得したデータと比較
// 前回取得したデータより古いデータの場合は無効
if (self.latestLocation.timestamp.timeIntervalSince1970 > location.timestamp.timeIntervalSince1970) {
NSLog(@"前回取得したデータより古いデータの場合は無効");
return NO;
}
// 前回取得した位置情報との距離を計算
double distance = fabs([self.latestLocation distanceFromLocation:location]);
// 前回取得した位置情報の精度
double latestAccuracy = self.latestLocation.horizontalAccuracy;
// 前回取得したデータからの移動距離が取得精度以下 かつ 前回の取得精度以下の精度の場合は無効
// ※ただし、horizontalAccuracyは取得できる値よりも精度が高い気がするので、適当に1/2にして使用した
BOOL isAcceptableDistance = (distance < latestAccuracy / 2.0);
BOOL isLessAccuracy = (latestAccuracy >= location.horizontalAccuracy);
if (isAcceptableDistance && isLessAccuracy) {
NSLog(@"前回取得したデータからの移動距離が取得精度以下 かつ 前回の取得精度以下(%.1fm)の精度の場合は無効", latestAccuracy);
return NO;
}
// 有効な位置情報
NSLog(@"有効データ");
return YES;
}
/**
地図上にポイントを描画
*/
- (void)drawPointOnMap:(MKMapView *)map location:(CLLocation *)location title:(NSString *)title {
MKPointAnnotation *pointAnnotation = [MKPointAnnotation new];
pointAnnotation.coordinate = location.coordinate;
pointAnnotation.title = title;
[map addAnnotation:pointAnnotation];
}
/**
地図上にルートを描画
*/
- (void)drawRootOnMap:(MKMapView *)map from:(CLLocation *)from to:(CLLocation *)to {
CLLocationCoordinate2D *coordinates = malloc(sizeof(CLLocationCoordinate2D) * 2);
coordinates[0] = from.coordinate;
coordinates[1] = to.coordinate;
MKPolyline *polyline = [MKPolyline polylineWithCoordinates:coordinates count:2];
[map addOverlay:polyline];
}
@end
おまけ 1. Kalman Filter
カーナビやアポロ計画などにも使用されているKalman Filterを利用すると、GPSの測定データの軌跡を修正することができるそうです。
位置情報の取得精度が最高精度ではない時に、ワープした位置情報をフィルターするのに使用できそうです。
iOSで使用できそうなライブラリは、Hypercubesoft/HCKalmanFilterがありました。
正直なところよくわからないので、ライブラリと関連記事の紹介のみです。
おまけ 2. マップマッチング
計測した位置情報の経路を道路に沿って綺麗に見せることのできる、マップマッチングを使用すると、移動経路がすごく綺麗に表示できます。
調べたかきりでは、iOS標準のMapKitでは提供されていない機能なので、以下の方法でマップマッチングをアプリ内に組み込むことができます。
Google Maps Roads APIを利用する
位置情報の配列をWebAPIに渡すと、マップマッチングを行った位置情報を返してくれるAPIがあるようです。
一度に100点までリクエストを投げられます。
有料なので、リアルタイムに使うのは難しいそうです。
- https://developers.google.com/maps/documentation/roads/snap
- Google Cloud Japan公式ブログ - 教えて Google Maps API(中級編): GPS 情報を道路にスナップする
- multisoup - Roads API を使って道路へスナップする
参考
Nike+を超える高精度位置情報フィルターの作り方 — 位置情報を正確にトラッキングする技術 in iOS
iPhoneアプリ開発の虎の巻 - CLLocationManager