SPIDERPLUS Tech Blog

建設SaaS「スパイダープラス」のエンジニアとデザイナーのブログ

iOSで地図画像上に現在位置情報を表示する

こんにちは。

スパイダープラスでiOSエンジニアをしているパッタイです。

iOSアプリの開発業務で、地図上に現在位置表示する方法を調べる機会がありました。
今回はそれを紹介したいと思います。

やりたいこと

iOSアプリで、国土地理院の地図などから切出した地図画像上に任意の位置情報を表示します。例えば、下の画像のように表示するイメージです。

おなじみの光景

iOSで地図を扱うにはMapKitがお馴染みです。MapKitなら地図上の現在位置情報を簡単に扱えますが、任意に切り出した地図画像上での位置情報は扱えません。

そのため、Core Locationを使って地図画像上に現在位置を表示できるようにします。

 

今回実現したいことをまとめると

  • iOSアプリで地理院地図などから切り出した地図画像上に、現在位置を表示させる。

そのために、

  • 画像内の任意の位置情報にいるときに画像内のxy平面座標計算をする。
  • 地図画像内のxy平面座標中心に円で表示させる。(今回の記事では扱いません。)

 

これら2つを実現させます。

前提

切り出した地図画像内の任意の位置情報のxy平面座標を計算するには、前提条件として地図画像内で2点の基準点の位置情報(緯度・経度)が必要です。(2点の基準点のxy平面座標も事前に分かっているものとします)。

 

地図画像内で2点の基準点の位置情報(緯度・経度)が分かれば、地図画像内の3点目の位置情報(緯度・経度)から画像上のxy平面座標を算出することが可能になります。

 

では早速進めていきましょう。

位置情報の許可系の設定

まず、実際の位置情報を活用するために、許可設定の実装が必要です。

info.plistにロケーションサービス使用許可のため、

NSLocationWhenInUseUsageDescriptionもしくは、NSLocationAlwaysAndWhenInUseUsageDescriptionを追加します。

  • NSLocationWhenInUseUsageDescriptionはアプリがフォアグラウンドで実行されていて、位置情報にアクセスする場合に指定します。
  • NSLocationAlwaysAndWhenInUseUsageDescriptionはアプリがバックグラウンドで実行されていて、位置情報にアクセスする場合に指定します。

 

次にiOSバイスでの位置情報取得には、Core Locationフレームワークを使用するため、CoreLocationをimportします。

import CoreLocation

 

次にソースコードに位置情報の許可リクエスト処理を記述します。

let locationManager = CLLocationManager()
// 以下呼び出しでユーザーに位置情報の許可をリクエスト
locationManager.requestWhenInUseAuthorization()

 

位置情報の許可をリクエストをすると以下のようなダイアログが表示されます。

(許可を選びます。)

 

位置情報の利用許可ステータスによって分岐処理する場合の記述例も紹介します。

if locationManager.authorizedWhenInUse == .authorizedWhenInUse {
    // 使用中のみ利用許可されているときに実施する処理
}

位置情報の利用許可のステータスは以下です

定義

許可状態の説明

0

notDetermined

未選択

1

restricted

ペアレンタルコントロールなどの影響で制限中

2

denied

利用拒否

3

authorizedAlways

常に利用許可

4

authorizedWhenInUse

使用中のみ利用許可

現在地の表示

位置情報取得の準備

位置情報取得の各種セットアップを行います。

func setup() {
    // 現在位置取得処理のデリゲート処理設定
    locationManager.delegate = self
    // 測位の精度を指定(最高精度)
    locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
    // 最小更新距離(メートル単位)
    locationManager.distanceFilter = 1.0
    // バッテリー消費量を抑えるための設定(default:true)
    locationManager.pausesLocationUpdatesAutomatically = false
}
desiredAccuracy

desiredAccuracyの設置値については以下を設定していきます。

今回は最高精度で設定します。

desiredAccuracy

精度

kCLLocationAccuracyBestForNavigation

デフォルト

kCLLocationAccuracyBest

最高精度

kCLLocationAccuracyNearestTenMeters

10m以内

kCLLocationAccuracyHundredMeters

100m以内

kCLLocationAccuracyKilometer

1km以内

kCLLocationAccuracyThreeKilometers

3km以内

位置情報取得開始・停止

以下を実行して位置情報の取得を開始します。

func startLocation() {
    // 現在位置を取得開始
    locationManager.startUpdatingLocation()
}

停止する場合は以下を実行します。

func stopLocation() {
    // 現在位置を取得停止
    locationManager.stopUpdatingLocation()
}

位置情報が更新されたときに呼ばれるデリゲートメソッド

デリゲートメソッドを実装することで、現在地の位置情報が変化した時に位置情報を取得可能です。

func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
    if let location = locations.last {                    
        print("緯度:", location.coordinate.latitude)
        print("経度:", location.coordinate.longitude)
        print("水平方向の位置精度(m単位):", location.horizontalAccuracy)
    }
}

CLLocationManagerインスタンスからも直接現在地の情報を取得できます。

locationManager.location?.coordinate.latitude   // 緯度
locationManager.location?.coordinate.longitude  // 経度
locationManager.location?.horizontalAccuracy    // 水平方向の位置精度(m単位)

地図上の2点の緯度経度情報から現在位置のxy平面座標を算出するときに使う3つの計算ロジック

では、コードと実行結果とともに、どんな風に進めていくかを見ていきましょう。

1. 地図上の2地点間の緯度経度の実測距離計算

これはCLLocationを使用して計算することができます。

 CLLocation distance(from:)

/// A地点-B地点の実測距離(m)を計算
/// - Parameters:
///   - fromLat: A地点の緯度
///   - fromLng: A地点の軽度
///   - toLat: B地点の緯度
///   - toLng: B地点の軽度
/// - Returns: A地点-B地点の実測距離(m)
func getDistance(fromLat: Double, fromLng: Double, toLat: Double, toLng: Double) -> Double {
    let locationA = CLLocation(latitude: fromLat, longitude: fromLng)
    let locationB = CLLocation(latitude: toLat, longitude: toLng)
    return locationA.distance(from: locationB)
}

2. 地図上の2地点間の緯度経度の方向角の計算

地球を球体とみなした球面三角法で計算します。

/// - Parameters:
///   - fromLat: from緯度
///   - fromLng: from軽度
///   - toLat: to緯度
///   - toLng: to軽度
/// - Returns: 角度(単位°)
func getAngle(fromLat: Double, fromLng: Double, toLat: Double, toLng: Double) -> Double {
    let fromLat = fromLat * .pi / 180
    let fromLng = fromLng * .pi / 180
    let toLat = toLat * .pi / 180
    let toLng = toLng * .pi / 180

    let diffLng = toLng - fromLng
    let y = sin(diffLng)
    let x = cos(fromLat) * tan(toLat) - sin(fromLat) * cos(diffLng)

    let p = atan2(y, x) * 180 / .pi

    if p < 0 {
        return p + 360
    }
    return p
}

3. 現在位置の平面座標距離の算出

地図上の基準点と現在地の実測距離と地図上の2地点間の緯度経度の実測距離の比率から計算します。

4. 地図上の基準点 のxy平面座標から現在位置地点のxy平面座標の算出

基準点(原点)と半径rと角度θから三角関数(cos, sin)を使って現在位置地点の座標計算をします。

座標(0, 0) と座標(rcosθ, rsinθ)を結ぶ直線をr、x軸に対するrの角度をθとすると、
底辺の長さはrcosθ、高さはrsinθとなります。現在位置座標は(x, y) = (rcosθ, rsinθ)です。

/// 現在位置の座標を計算
/// - Parameters:
///   - origin: 基準点
///   - r: 基準点と現在地の平面座標距離
///   - angle: 2地点間の緯度経度の方向角(単位円としたときの角度θ)
/// - Returns: 現在位置の座標
func getPoint(origin: CGPoint, distance r: Double, angle: Double) -> CGPoint {
    let radian = angle * .pi / 180 // ラジアン
    let x = origin.x + r * cos(radian)
    let y = origin.y + r * sin(radian)
    return CGPoint(x: x, y: y)
}

これらをもとに、A点を(0, 0), B点を(1, 0)とした場合の現在位置のxy平面座標情報を計算する方法について計算してみます。

例題

では、実際に任意の切り出した地図画像上に円で現在位置を表示させるために、座標の計算をしてみます。

 

以下の画像は国土地理院のサイトから東京駅付近の地図を切り出して使っています。

(引用元URL: https://maps.gsi.go.jp/#14/35.673544/139.758496/&base=std&ls=std&disp=1&vs=c1g1j0h0k0l0u0t0z0r0s0m0f0

  • 東京駅A地点(緯度, 経度)=(35.681492,139.766521)
  • 東京駅から真東方向のB地点(緯度, 経度)=(35.681492,139.788666)

例題1

例えばA地点のxy平面座標が(x, y) = (0, 0)、B地点のxy平面座標が(x, y) = (1, 0)とした場合に、AB直線上のC地点(緯度、経度)=(35.681492,139.776821)に現在位置がある場合の座標計算方法を考えてみます。

この場合のC地点のxy平面座標計算方法は以下です。

1. AB間の実測距離を算出する。

print("AB間の実測距離(m):", getDistance(fromLat: 35.681492, fromLng: 139.766521, toLat: 35.681492, toLng: 139.788666))

実行結果

AB間の実測距離(m): 2004.6727710827702

2. AC間の実測距離を算出する。

print("AC間の実測距離(m):", getDistance(fromLat: 35.681492, fromLng: 139.766521, toLat: 35.681492, toLng: 139.776821))

実行結果

AC間の実測距離(m): 932.4059400387969

3. AC間のxy平面座標距離は比率計算で算出する。

print("AC間のxy平面座標距離:", 932.406 / 2004.673)

実行結果

AC間のxy平面座標距離: 0.465116255868164

4. AC間の方向角を算出する。

print("A→C間の方向角:", getAngle(fromLat: 35.681492, fromLng: 139.766521, toLat: 35.681492, toLng: 139.776821))

実行結果

A→C間の方向角: 89.99354164479665

国土地理院で切出した図面は図面真上方向が真北方向になり、直線ABはそれに対して垂直に交わっています。そのため、直線AB上の点は三角関数の単位円にしたときの角度θ=0になります。

5. C地点のxy座標を算出する。

let r = 0.465       // AC間のxy平面座標距離
let angle = 90 - 90 // 三角関数の単位円にしたときの角度θ
let point = getPoint(origin: .zero, distance: r, angle: angle)
print("pointC (x, y) = \(point)")

実行結果

pointC (x, y) = (0.465, 0.0)

例題2

続けて、以下の神田駅のD地点(緯度, 経度)=(35.691929,139.770866)の座標計算をしてみます。

1. AD間の実測距離計算

print("AD間の実測距離(m):", getDistance(fromLat: 35.681492, fromLng: 139.766521, toLat: 35.691929, toLng: 139.770866))

実行結果

AD間の実測距離(m): 1222.9936937870013

2. AD間のxy平面座標距離を比率計算する。

print("AD間のxy平面座標距離:", 1222.994 / 2004.673)

実行結果

AD間のxy平面座標距離: 0.6100715677818777

3. AD間の方向角を算出する。

print("A→D間の方向角:", getAngle(fromLat: 35.681492, fromLng: 139.766521, toLat: 35.691929, toLng: 139.770866))

実行結果

A→D間の方向角: 18.68080895479806

4. D地点のxy座標を算出する。

let r = 0.61             // AD間のxy平面座標距離
let angle = 90.0 - 18.68 // 三角関数の単位円にしたときの角度θ
let point = getPoint(origin: .zero, distance: r, angle: angle)
print("pointD (x, y) = \(point)")

実行結果

pointD (x, y) = (0.19537222270203794, 0.5778665024003946)

今後について

今回、地図上の緯度・経度情報から現在位置のxy平面座標の計算方法を調査をしました。

三角関数を活用して、原理原則を理解した上で実装することで、多くの学びを得ることができたと思います。

 

今回お送りした現在位置表示のための技術は、SPIDERPLUSのGPS情報を用いて自動で撮影位置を記録する用途などで使われています。

建設業の長く抱える課題は社会課題そのものでもあり、SPIDERPLUSというプロダクトを通して現場の悩みごとに最適解を提示していくことができるよう、今後もアンテナをはっていきたいと思います。

 

技術ブログではこれからもスパイダープラスの開発チームが使っているものや、関心のあることについてとりあげていきます。

最後に

スパイダープラスでは仲間を募集中です。

スパイダープラスにちょっと興味が出てきたなという方がいらっしゃったらお気軽にご連絡ください。

https://spiderplus.co.jp/contact/

参考