Rotate 'Google Map' by RotateSensor in Kotlin

WindSekirun (wind.seo)·2022년 4월 26일
0

이 글은 기존 운영했던 WordPress 블로그인 PyxisPub: Development Life (pyxispub.uzuki.live) 에서 가져온 글 입니다. 모든 글을 가져오지는 않으며, 작성 시점과 현재 시점에는 차이가 많이 존재합니다.

작성 시점: 2018-03-22

도입

최근 한 고객사의 앱을 추가개발 하는 일이 있었는데, 요청 사항중 '네비게이션 모드' 이런 게 있었다.

너무 추상적이라 잘 표현을 못하겠지만, 요건은 '기기가 돌아가면 그 방향으로 지도가 자동으로 회전하고, 이동하면 내 위치가 자동으로 갱신된다' 라는 조건인 것 같다.

그래서 하는 겸에 기존에 잘 사용했던 Dagger 용 사용자 위치 계측 모듈에 회전 센서 기능을 붙여보기로 했다.

이론

구글 지도 안드로이드 API 문서를 보면, 베어링(방향) 이란 페이지가 있다. 흔히 지도의 수직선이 가리키는 방향으로, 북쪽을 기준으로 시계방향으로 측정된다.

즉, 내가 남향을 보고 있으면 현재 베어링은 180도가 되는 식이다.

이 방향을 측정하기 위해 SensorManager의 TYPE_ROTATION_VECTOR 센서와 현재 위치에 대한 GeomagneticField (지표면 자기장) 의 편각을 이용해서 움직일 베어링을 실시간으로 측정하고 그 베어링 값을 기반으로 구글 지도를 움직여보려고 한다.

단, 이 글에서 실시간으로 위치 계측하는 방법까진 소개하지 않을 예정이니, LocationManagerFusedLocationApi를 이용하여 현재 위치에 대한 Location 객체를 가져오는 것 까진 직접 찾아보는 것이 좋을 것 같다.

GeomagneticField를 이용한 편각 계산

GeomagneticField 클래스의 생성자는 아래의 모습을 가지고 있다.

 /**
     * Estimate the magnetic field at a given point and time.
     * 해당 위치와 시간에 대한 지표면 자기장 계산

     * @param gdLatitudeDeg
     *            Latitude in WGS84 geodetic coordinates -- positive is east.
     *            WGS84에 따른 위도 데이터 -- 양수이면 동쪽
     * @param gdLongitudeDeg
     *            Longitude in WGS84 geodetic coordinates -- positive is north.
     *            WGS84에 따른 경도 데이터 -- 양수이면 북쪽
     * @param altitudeMeters
     *            Altitude in WGS84 geodetic coordinates, in meters.
     *            WGS84에 따른 고도 데이터, 단위는 meter
     * @param timeMillis
     *            Time at which to evaluate the declination, in milliseconds
     *            since January 1, 1970. (approximate is fine -- the declination
     *            changes very slowly).
     *            측정이 이루어지는 Unix Timestamp 시간, mills 단위
     */
public GeomagneticField(float gdLatitudeDeg,
                            float gdLongitudeDeg,
                            float altitudeMeters,
                            long timeMillis)

그리고 해당 클래스에는 편각(Declination) 을 구하는 메서드가 있다.

/**
     * @return The declination of the horizontal component of the magnetic
     *         field from true north, in degrees (i.e. positive means the
     *         magnetic field is rotated east that much from true north).
     *         북쪽에서의 자기장에 대한 수평 성분의 기울기 (단위는 도)
     *         즉, 양수이면 자기장이 북쪽에서 그만큼 동쪽으로 회전했다는 것으로 의미됨
     */
    public float getDeclination() {
        return (float) Math.toDegrees(Math.atan2(mY, mX));
    }

이 편각 데이터는 센서에서 받아온 데이터를 가공할 때 사용되므로 필드로 보관하면 된다.

val geomagneticField = GeomagneticField(it.latitude.toFloat(), it.longitude.toFloat(), it.altitude.toFloat(), System.currentTimeMillis())
mDeclination = geomagneticField.declination

Sensor 사용, 데이터 받기

센서 기능을 사용하기 위해서는 SensorManager의 구현이 필요하다. 여기서는 RichUtilsKt 가 제공하는 Context.sensorManager 기능을 이용해서 SensorManager 객체를 얻고, Sensor.TYPE_ROTATION_VECTOR 를 얻는다.


private val mSensorManager: SensorManager by lazy { mContext.sensorManager }
private val mRotVectorSensor: Sensor by lazy { mSensorManager.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR); }

lazy에 대한 자세한 기능이나 활용은 이미 좋은 글이 많이 있으므로, 그 글을 살펴보는 것이 좋다.

그리고 해당 클래스에 SensorEventListener를 구현하고, Sensor 측정을 시작한다.

mSensorManager.registerListener(this, mRotVectorSensor, SensorManager.SENSOR_STATUS_ACCURACY_LOW)

이제 센서의 데이터가 바뀌면, 아래 두 메서드에 값이 들어올 것이다. 여기서는 onSensorChanged만 사용한다.

override fun onSensorChanged(event: SensorEvent?) {

}

override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
    
}

베어링 측정

베어링 값을 측정하기 위해서는 아래의 과정을 거친다.

  1. onSensorChanged의 SensorEvent 파라미터는 Nullable 하므로 null-safe 하게 만들어준다.
  2. 해당 값이 TYPE_ROTATION_VECTOR 로 통해 얻어진 값인지 조사한다. (여러개의 센서를 활용할 경우에 대비)
  3. SensorManager의 getRotationMatrixFromVector 라는 메서드를 사용하여 RotationVector를 RotationMatrix로 변경한다. 이 때 사용되는 mRotationMatrix 값은 FloatArray 형태여야 하는데, 반드시 3x3(FloatArray(9)) 또는 4x4(FloatArray(16)) 값을 가져야 한다.
  4. SensorManager의 getOrientation 라는 메서드를 사용하여 RotationMatrix를 기반으로 기기의 방향을 읽어낸다. 이 때 얻어낼 orientation 값은 세 개가 있다.
    1. value[0]: Azimuth (방위각), -z 축에 대한 회전 각도
    2. value[1]: Picth (피치), y축에 대한 회전 각도
    3. value[2]: Roll (롤), x축에 대한 회전 각도
  5. 라디안 단위로 측정된 각도를 각도 단위로 변환하고, 편각 값을 더하면 베어링 값이 완성된다.
  6. 이 클래스는 모듈이므로 RxJavaPublishSubject를 이용하여 subscriber 들에 베어링 값을 전달한다. 이 subscriber 는 지도를 움직일 역할을 할 것이다.

위 과정을 코드로 작성하면 다음과 같다.

override fun onSensorChanged(event: SensorEvent?) {
    event?.sensor?.run {
        if (type == Sensor.TYPE_ROTATION_VECTOR) {
            SensorManager.getRotationMatrixFromVector(mRotationMatrix, event.values)
            val orientation = FloatArray(3)
            SensorManager.getOrientation(mRotationMatrix, orientation)
            mBearing = (Math.toDegrees(orientation[0].toDouble()) + mDeclination).toFloat()

            mBearingCallback.onNext(mBearing)
        }
    }
}

지도 이동

mTracker.getBearingObservable().subscribe {
    val position: CameraPosition = CameraPosition.Builder()
        .target(mGoogleMap.cameraPosition.target)
        .zoom(mGoogleMap.cameraPosition.zoom)
        .tilt(mGoogleMap.cameraPosition.tilt)
        .bearing(it)
        .build()

    mGoogleMap.moveCamera(CameraUpdateFactory.newCameraPosition(position))
}

CameraPosition 값을 만들 때 위치, 확대 수준, 틸트 수준은 기존에 사용하던 값을 사용하고, 베어링 값만 계산한 베어링 값을 사용해서 지도를 움직이게 한다.

마무리

처음 개발 검토 들어왔을 때 무거운 작업인 줄 알았으나, 원리를 이해하면 구현하는 코드 자체는 짧게 된다.

물론 그 원리가 조금 난이도 있다는 것이 문제지만 (._.

profile
Android Developer @kakaobank

0개의 댓글