2015年10月8日木曜日

Swift2で二地点の緯度・経度からその距離を計算するには?

二地点の緯度・経度からその距離を計算する(日本は山だらけ〜)

のJava版をSwift2に移植しました。数学は分かりません。理論も分かりません。でもSwiftとJavaは分かるので移植はできるのです(^^);

オリジナルがMITライセンスなので、このコードもリスペクトを込めてMITライセンスとします。


//
//  GeoUtil.swift
//

import Foundation


/**
*  二地点の緯度・経度からその距離を計算する(日本は山だらけ〜) - http://yamadarake.jp/trdi/report000001.html
*/
@objc(GeoUtil) class GeoUtil: NSObject {
    
    
    static let BESSEL_A: Double = 6377397.155;
    static let BESSEL_E2: Double = 0.00667436061028297;
    static let BESSEL_MNUM: Double = 6334832.10663254;
    
    static let GRS80_A: Double = 6378137.000;
    static let GRS80_E2: Double = 0.00669438002301188;
    static let GRS80_MNUM: Double = 6335439.32708317;
    
    static let WGS84_A: Double = 6378137.000;
    static let WGS84_E2: Double = 0.00669437999019758;
    static let WGS84_MNUM: Double = 6335439.32729246;
    
    static let BESSEL: Int = 0;
    static let GRS80: Int = 1;
    static let WGS84: Int = 2;
    
    
    static func deg2rad(deg: Double) -> Double {
        return deg * M_PI / 180.0;
    }
    
    
    static func calcDistHubeny(lat1: Double, lng1: Double, lat2: Double, lng2: Double, a: Double, e2: Double, mnum: Double) -> Double {
        
        let my: Double = deg2rad((lat1 + lat2) / 2.0);
        let dy: Double = deg2rad(lat1 - lat2);
        let dx: Double = deg2rad(lng1 - lng2);
        
        let sinVal: Double = sin(my);
        let w: Double = sqrt(1.0 - e2 * sinVal * sinVal);
        let m: Double = mnum / (w * w * w);
        let n: Double = a / w;
        
        let dym: Double = dy * m;
        let dxncos: Double = dx * n * cos(my);
        
        return sqrt(dym * dym + dxncos * dxncos);
        
    }
    
    
    static func calcDistHubeny(lat1: Double, lng1: Double, lat2: Double, lng2: Double) -> Double {
        return calcDistHubeny(lat1, lng1: lng1, lat2: lat2, lng2: lng2, a: GRS80_A, e2: GRS80_E2, mnum: GRS80_MNUM);
    }
    
    
    static func calcDistHubery(lat1: Double, lng1: Double, lat2: Double, lng2: Double, type: Int) -> Double {
        switch(type) {
        case BESSEL:
            return calcDistHubeny(lat1, lng1: lng1, lat2: lat2, lng2: lng2, a: BESSEL_A, e2: BESSEL_E2, mnum: BESSEL_MNUM);
        case WGS84:
            return calcDistHubeny(lat1, lng1: lng1, lat2: lat2, lng2: lng2, a: WGS84_A, e2: WGS84_E2, mnum: WGS84_MNUM);
        default:
            return calcDistHubeny(lat1, lng1: lng1, lat2: lat2, lng2: lng2, a: GRS80_A, e2: GRS80_E2, mnum: GRS80_MNUM);
        }
    }
    
    
}

iOS9で位置情報を使う定期バックグラウンド処理をする場合のバッテリー消費を抑えるには?

iOSでのバックグラウンド処理の制限

iOSでのバックグラウンド処理は非常に制限されているが、いくつか方法はある。しかし、「定期的にバックグラウンド処理をする」という条件になるとかなり限られる。

どのような方法を選択できるかはアプリのタイプや目的によるが、アプリによらず選択できるであろう方法は少なくとも2つある。

Background fetch

iOSがアプリの利用頻度などから「非定期に」バックグラウンド処理を動作させるので使えない。

Remote Notifications

サーバーからのNotificationが届くタイミングでバックグラウンド処理を起動する方法だが、サーバーサイドの実装が必要な上、Notificationが届くタイミングはまちまちで、最悪の場合は届かないこともあるので、苦労の割に確実性に欠ける。


AndroidのAlarmManagerの代替は無いのか?

調べた限り、iOSではAndroidでいうところのAlarmManagerのようなバックグランド処理はできない。iOSで言うところの「バックグラウンド」とは、タスクマネージャーにプロセスが残っている状態のことである。つまり、ユーザーがタスクマネージャーからアプリを終了させたらバックグラウンド処理はできない。


位置情報の更新を利用した定期バックグラウンド処理を採用

幸い今回開発中のアプリは位置情報を利用するタイプのアプリなので、「Location updates」のcapabilityを利用して定期的にバックグラウンド処理をさせることができた。


位置情報の更新によるバッテリー消費を抑えるために考えた2つのこと(失敗)

しかし、ここで気になるのは、バックグラウンドで位置情報を更新し続けることによるバッテリーの消費がどれだけのものなのかということ。

そこで、このあたりを参考にした。

Energy Efficiency Guide for iOS Apps: Reduce Location Accuracy and Duration

一番始めに考えたのは、iOS9から導入されたrequestLocation()を利用することだった。タイマーで必要な時だけrequestLocation()を使って位置情報を取得すればバッテリーの消費を抑えられるのではないかと。

How to request a user's location only once using requestLocation – Swift 2 example code

しかし、この方法はダメ。requestLocation()は「Location updates」の処理のうちに入らないらしく、タイマー(プロセス)が止まってしまって定期処理を実現できなかった。

次に考えたのは、beginBackgroundTaskWithExpirationHandler()を使う方法。アプリがバックグラウンドになった時に一定の期間でstartUpdatingLocation()を呼び、すぐに停止させればバッテリーの消費を抑えられるのではないかと。

この方法もダメ。beginBackgroundTaskWithExpirationHandler()によるバックグラウンド処理は約3分間しかもたないうえ、処理が終わった時に新たにbeginBackgroundTaskWithExpirationHandler()を使えないので定期処理を実現できなかった。


startUpdatingLocation()を使いつつ、精度でバッテリー消費をコントロール

やはりタイマー(プロセス)をバックグラウンドで動作させ続けるためにはlocationManager.startUpdatingLocation() を使うことが必要。そこで、「Reduce Accuracy of Standard Location Updates Whenever Possible」のTIPsを使うことにした。つまり、ギリギリまで精度を下げて、できるだけバッテリー消費を抑える作戦。

ところで、pausesLocationUpdatesAutomatically = falseにしないとstartUpdatingLocation()を使っていても15分程度で止まるので注意。また、startMonitoringSignificantLocationChanges()ではバックグラウンド動作しなかったので注意。


実験

指定できるオプションはいくつかあるが、上に行くほどバッテリー消費が激しくなる。

Accuracy Constants - Core Location Constants Reference

ここでは、バッテリー100%の状態からstartUpdatingLocation()して、タイマーを使って定期的にWEB APIを叩いてその結果をNotificationするという作業を12時間続けて何パーセントまで減るかを見た。使用した機体はiPhone5。

まず、この処理をさせない場合は12時間放置で100%だった。実際は100%のマージンが大きすぎるのだろうと思うが、これがユーザーが見る現実だ。

次に「数百メートル」(kCLLocationAccuracyHundredMeters)に設定して15分に一回動作するケースを見たら76%だった。30分に1回にしたら78%。少しは効果があるが、やはりほとんどは位置情報の更新にエネルギーが費やされていると考えられる。

次はいきなり「3キロ」(kCLLocationAccuracyThreeKilometers)に設定して15分に一回動作するケースを見たら84%だった。かなり消費を抑えられる。

問題はkCLLocationAccuracyThreeKilometersに設定した場合の位置情報の精度がどれくらいのものなのかということだ。

調べた限りでは、かなり正確な場合もあるし、数百メートルずれている場合もあったが、キロ単位でずれていることは無かった


結論

位置情報の更新を使って定期的なバックグラウンド処理をする場合は、できるだけkCLLocationAccuracyThreeKilometersを設定し、タイマーでの動作回数を少なくすると良いだろう。