カメラ

HERE SDK for iOS では、マップのビューを変更する方法がいくつか用意されています。 地図のスタイルを使用すると、地図の外観を変更できますが、カメラを使用してさまざまな視点から地図を参照できます。

たとえば、 HERE SDK for iOS では、ターゲットの場所の設定、地図のチルト、ズームイン / ズームアウト、ベアリングの設定を行うことができます。

メモ

一目で確認できます

  • CameraLite からアクセス可能なを使用 mapView.camera して、地図のビューを操作します。
  • を呼び出し camera.updateCamera(cameraUpdate:) て、すべてのカメラのプロパティを 1 回の移動で適用します。
  • を登録して、カメラへの変更を監視 CameraObserverLiteします。
  • CameraLimitsLite によってアクセス可能なオブジェクトに制限を設定して、カメラに制約 camera.limitsを設定します。
  • geoToViewCoordinates(geoCoordinates:) およびを使用 viewToGeoCoordinates(viewCoordinates:)して、ビューと地理的な座標空間を変換します。
  • にアクセス camera.boundingBoxして、現在表示されている領域の境界を取得します。

デフォルトでは、カメラは地図の中央に配置されます。 鳥の目から見てまっすぐ下を見ると、地図は 北を向いています。 これは、デバイスの上端が地図の北を指していることを意味します。

地図を回転します

方位角を設定することで、地図の向きを変更できます。 「 Bearing 」は北から時計回りに度でカウントされるナビゲーション用語です。

図 : ベアリングの方向を設定します。

デフォルトでは、地図は 0 ° の方位に設定されています。 上の図に示されて いるように、 45 ° の角度を設定すると、地図が反時計回りに回転し、地図上で方位の方向が新しい上向きになり、デバイスの上端を指します。 これは、ハイキング中に特定の方向に紙の地図を保持して回転するのと似ています。 マップの上端が目的の方向を向いている場合は、自分自身の向きを変更する方が簡単です。 ただし、これが必ずしも真の北の方向であるとは限りません( bearing=0 ° )。

地図を傾けます

このカメラを使用して、 2D 地図の平坦な表面を 3D の遠近法にして、たとえば、地平線に向かって見える可能性のある長距離道路を表示することもできます。 デフォルトでは、地図はチルトされません(チルト = 0 ° )。

チルト角度は、ターゲット位置の垂直軸から計算されます。 観察者の下を直進するこの方向を 直下視することを、直下視と呼びます。 以下の図に示すように、地図を 45 ° 傾けると、カメラの鳥瞰図が 3D の視点に変わります。 これによってカメラが効果的に移動されますが、それ以降のチルト値は常にカメラのデフォルトの位置から適用されます。 チルト値が制限値を超えるか、または下回ると、マップが表示されない可能性があります。 これらの絶対値は、最小値 () の定数CameraLimitsLite.minTiltとして使用することも、実行時に取得することもできます。

// Error cannot happen here as we set the allowed max value for zoom level 12.
try! cameraLimits.setMaxTilt(degreesFromNadir: CameraLimitsLite.getMaxTiltForZoomLevel(zoomLevel: 12))

このようにして、絶対範囲内で独自のカメラ制限を指定できます。

図 : 地図を傾けます。

地図の場所を変更します

新しいカメラターゲットを設定することで、地図の位置を即座に変更できます。 既定では、ターゲットは地図の中央に配置されます。 全体的に、カメラの使用は非常に簡単です。 次のコードスニペットの例を参照してください。

// Set the map to a new location - together with a new zoom level and tilt value.
mapView.camera.setTarget(GeoCoordinates(latitude: 52.530932, longitude: 13.384915))
mapView.camera.setZoomLevel(14)
mapView.camera.setTilt(degreesFromNadir: 60)

または、を設定すること CameraUpdateLiteで、 1 回の移動で複数の変更を適用できます。

カメラでは、特定の値を制限するカスタム範囲を指定することもできます。また、ユーザーがダブルタップジェスチャを実行して地図を操作した場合などに、これらの値の更新を監視する方法も提供されます。

新しいターゲットアンカーポイントを設定することで、 x=0.5, y=0.5 の位置にあるデフォルトのカメラアンカーポイントを変更できます。これは、マップ ビューの中心を示します。 ターゲットの位置は、回転やチルトなどのすべてのプログラム的なマップ変換、または新しいターゲットの位置の設定に使用されます。 ピンチ回転2 本の指でのパン による地図のチルトなどの既定のジェスチャには影響しません。 次の方法で、新しいターゲットアンカーポイントを設定します。

let transformationCenter = Anchor2D(horizontal: normalizedX,
                                    vertical: normalizedY)
camera.targetAnchorPoint = transformationCenter

アンカーポイントは、正規化された座標で指定できます。 (0, 0) は、マップ ビューの左上隅と右下隅 (1,1) を示します。 そのため、アンカーを (1,1) に設定すると、新しいターゲットの場所がマップ ビューの右下隅に表示されます。 マップ ビュー外の値がクランプされます。

ターゲットに新しいアンカーポイントを設定しても、新しいターゲットの場所を設定するか、またはプログラムでマップをチルトまたは回転するまで、マップには表示されません。 タイル角度は、アンカーの水平座標に配置されます。 同様に、回転の中心点はアンカーと同じです。

この チュートリアルでは、ターゲットのアンカーポイントを使用する方法の例を示します。 マップ ビューの特定のポイントでズームインする方法を示します。

カメラの変更を確認します

オブザーバを追加し CameraObserverLite 、プロトコルに準拠することで、カメラ(およびマップ)が変更されたときにクラスに通知できます。

func onCameraUpdated(_ cameraUpdate: CameraUpdateLite) {
    let mapCenter = cameraUpdate.target
    print("Current map center location: \(mapCenter). Current zoom level: \(cameraUpdate.zoomLevel)");
}

func addCameraObserver() {
    mapView.camera.addObserver(self)
}

また、地図との高速でスムーズなインタラクションを手動で実行できます。 既定では、ダブルタップするとズームインし、パンすると指で地図を動かして移動できます。 デフォルトのマップビヘイビアーの詳細については、「ジェスチャ」セクションを参照してください。

チュートリアル - 位置にアニメーションを作成します

新しいカメラターゲットを設定すると、地図上の任意の場所にすぐにジャンプできます。遅延はありません。 ただし、状況によっては、現在の場所から新しい場所にゆっくりと移動する地図を表示する方がわかりやすい場合があります。

このよう な XY への移動 方法は、現在の地理座標と新しい地理座標を補間することで実現できます。 緯度と経度のペアの各中間セットを新しいカメラターゲットとして設定できます。 幸い、このアニメーションによる移行は、 Apple の CADisplayLink で簡単に実現できます。 このクラスのインスタンスを使用して、カメラの更新内容を表示の更新レートと同期できます。結果として、地図がスムーズに移行されます。

はじめにでは、まずインターフェイスの外観を定義します。 すべてのアニメーションコードをアプリケーションコードの REST から分離しておくことをお勧めします。 このため、という名前の新しいクラスを作成します CameraAnimator。 使用量は次のようになります。

private let defaultZoomLevel: Double = 14
private lazy var cameraAnimator = CameraAnimator(mapView.camera)
...
cameraAnimator.moveTo(geoCoordinates, defaultZoomLevel)

このチュートリアルで紹介するのは以上です。 まず、依存関係として CameraLite オブジェクトを必要とする新しい CameraAnimator インスタンスを遅延的に作成します。 moveTo() この方法では、新しいカメラのターゲット位置と希望のズーム レベルを受け入れます。

次のステップで displayLink は、新しい CameraAnimator クラスにインスタンスを追加します。 また、後で displayLink 一時停止を解除した後に定期的に実行される selector メソッド animatorLoop も指定します。

private lazy var displayLink = CADisplayLink(target: self,
                                             selector: #selector(animatorLoop))

また、ループを変数 duration で実行できるようにしているため、そのデフォルトの duration を上書きできます。

private var animationDurationInSeconds: Double = 2

func setDurationInSeconds(animationDurationInSeconds: Double) {
    self.animationDurationInSeconds = animationDurationInSeconds
}

moveTo() このメソッドには線形アニメーションタイプを実装する予定です。そのため、現在の位置から離れた距離よりも短い距離でアニメーションが遅くなります。 デフォルトでは、上記で定義したように、実行ループは 2 秒後に停止します。

moveTo() メソッドの実装は、アニメーションの開始に注意する必要があります。 アニメーションは、終了後に自動的に停止する必要があります ( 後で表示します ) 。

func moveTo(_ destination: GeoCoordinates, _ targetZoom: Double) {
    currentCamera = CameraUpdateLite(tilt: camera.getTilt(),
                                     bearing: camera.getBearing(),
                                     zoomLevel: camera.getZoomLevel(),
                                     target: camera.getTarget())

    // Take the shorter bearing difference.
    let targetBearing: Double = camera.getBearing() > 180 ? 360 : 0

    targetCamera = CameraUpdateLite(tilt: 0,
                                    bearing: targetBearing,
                                    zoomLevel: targetZoom,
                                    target: destination)

    // Start the run loop to execute animatorLoop() periodically.
    startTimeInSeconds = -1
    displayLink.isPaused = false
    displayLink.add(to: .current, forMode: .common)
}

HERE CameraUpdateLite では、位置だけでなく、チルトや回転などの他のカメラパラメーターもアニメーション化するために、 2 つの新しいインスタンスを作成します。 1 つ目のインスタンスは現在のマップ状態を保持し、 2 つ目のインスタンスはアニメーションが停止したときに到達する必要がある終了状態を定義します。

現在設定されている傾きの値に関係tilt: 0なく、非傾きのマップ () にアニメーション化して戻します。 回転移動の場合、到達速度を上げるベアリング値を決定する必要があります。 非回転の地図の方位角は 0 ° で、 360 ° と同じです。したがって、現在の方位値を確認します。 たとえば、 200 ° の場合は、 360 ° に達するまでアニメーション化が速くなります。

displayLink インスタンスから正確な開始時間 startTimeInSeconds が取得されることに注意してください。ループが開始された後です。 displayLink.timestamp は、経過時間をミリ秒単位で把握できるように正確な Double 値を提供します。 基本的に、各実行サイクルの現在のタイムスタンプと前のタイムスタンプに関心があります。 これ frameDurationInSeconds により、各サイクルのを計算できます。 ループを呼び出すたびに、現在のカメラを更新する新しいフレームが作成されます。 ループがどのようにこれを行うかを見てみましょう。

@objc private func animatorLoop() {
    if startTimeInSeconds == -1 {
        // 1st frame, there's no previous frame.
        startTimeInSeconds = displayLink.timestamp
        previousTimeInSeconds = startTimeInSeconds
        return
    }

    let currentTimeInSeconds = displayLink.timestamp
    let elapsedTimeInSeconds = currentTimeInSeconds - startTimeInSeconds

    let frameDurationInSeconds = currentTimeInSeconds - previousTimeInSeconds
    let remainingFrames = (animationDurationInSeconds - elapsedTimeInSeconds) / frameDurationInSeconds

    // Calculate the new camera update.
    currentCamera = interpolate(currentCamera, targetCamera, remainingFrames)

    if elapsedTimeInSeconds >= animationDurationInSeconds {
        displayLink.isPaused = true
        camera.updateCamera(cameraUpdate: targetCamera)
        return
    }

    camera.updateCamera(cameraUpdate: currentCamera)
    previousTimeInSeconds = displayLink.timestamp
}

上記のように、新しい位置からどれだけ離れていても、 ( 呼び出した後の moveTo()) 経過時間 animationDurationInSeconds が経過した後でアニメーションを一時停止します。 これを行うには、合計期間に基づいて中間カメラの値を計算する必要があります。

通常、表示リンクは 60 FPS ( 1 秒あたりのフレーム数)で実行されます。 CPU の負荷が高いことが原因でドロップが発生した場合 remainingFrames は、前のサイクル時間に基づいてサイクルごとの推定値を計算します。 たとえば、一部のフレームをスキップする必要がある場合、必要なフレーム数を減らすことができます。 このようにして、各ステップの次の値を、目的の補間と同期するように調整できます。

補間法では、時間の経過とともに値が変化する速度 ( または遅く ) を定義します。 この目的のために、アニメーション全体で一定の速度を提供する線形アルゴリズムを使用することにしましたが、現在の位置とターゲットの位置の間の距離によって異なります。

実際の補間を開始する前に、次の値を入力する必要があります。

private func interpolate(_ currentCamera: CameraUpdateLite,
                         _ targetCamera: CameraUpdateLite,
                         _ remainingFrames: Double) -> CameraUpdateLite {
    let newTilt = interpolateLinear(currentValue: currentCamera.tilt,
                                    targetValue: targetCamera.tilt,
                                    remainingFrames: remainingFrames)

    let newBearing = interpolateLinear(currentValue: currentCamera.bearing,
                                       targetValue: targetCamera.bearing,
                                       remainingFrames: remainingFrames)

    let newZoomLevel = interpolateLinear(currentValue: currentCamera.zoomLevel,
                                         targetValue: targetCamera.zoomLevel,
                                         remainingFrames: remainingFrames)

    let newTargetLatitude = interpolateLinear(currentValue: currentCamera.target.latitude,
                                              targetValue: targetCamera.target.latitude,
                                              remainingFrames: remainingFrames)

    let newTargetLongitude = interpolateLinear(currentValue: currentCamera.target.longitude,
                                               targetValue: targetCamera.target.longitude,
                                               remainingFrames: remainingFrames)

    return CameraUpdateLite(tilt: newTilt,
                            bearing: newBearing,
                            zoomLevel: newZoomLevel,
                            target: GeoCoordinates(latitude: newTargetLatitude,
                                                   longitude: newTargetLongitude))
}

前述のように、さまざまなカメラパラメーターを補間することにしました。 このため、 5 つの異なる補間を計算する必要があります。

  • ズーム補間: 現在のズーム レベルからターゲットズーム レベルに補間します。
  • チルト補間: 現在のチルト値からターゲットのチルト値までを補間します。
  • ベアリング補間: 現在のベアリング角度からターゲットベアリング値までを補間します。 これにより、マップが効果的に回転します。
  • 緯度および経度補間: 両方とも、現在のターゲット位置から目的の新しいターゲットまでの単一の座標を補間します。

各補間を作成するコードは同じであるため、次の別々の方法で外部化しました。

private func interpolateLinear(currentValue: Double,
                               targetValue: Double,
                               remainingFrames: Double) -> Double {
    let delta = (currentValue - targetValue) / remainingFrames
    let newValue = currentValue - delta

    // Overflow check.
    if (currentValue < targetValue) {
        if newValue >= targetValue {
            return targetValue
        }
    } else {
        if newValue <= targetValue {
            return targetValue
        }
    }

    return newValue
}

線形アニメーションスタイルの場合、アルゴリズムは非常に単純です。現在の値とターゲットの終了値の差を、予想されるフレーム数で割った値を減算します。 アニメーションが安定して実行されると、アニメーションが停止したときに指定された終了値に到達します。 不安定な場合は、オーバーフローチェックを追加して、目的の値を超えないようにします。

結果の各値は、 animatorLoop() でマップ ビューを調整するために使用する新しい CameraUpdateLite インスタンスに追加されます。このインスタンスの呼び出し元は次のとおりです。

camera.updateCamera(cameraUpdate: currentCamera)

を呼び出す updateCamera() と、マップの外観が指定した値にただちに変更されます。 このコードは毎秒何度も実行されるため、人間の目に滑らかなアニメーションとして表示されます。

上記のコードスニペットを自由に操作して、さまざまな動作を試してください。

これは、を使用して、ある場所から別の場所へのカスタムトランジションを実装する方法の一例にすぎ CameraLiteません。 さまざまな補間法を使用することで、任意のアニメーションスタイルを実現できます。 クラシックな リボン トランジション(ズームアウトしてから、もう一度ズームイン)から、直線的に進む 他のトランジションへの直線的なトランジションまでを切り替えます。

」に一致する結果は 件です

    」に一致する結果はありません