From e523e0312e07af5282d226acff055b4dae4c2c83 Mon Sep 17 00:00:00 2001 From: MaraMincho <103064352+MaraMincho@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:42:11 +0900 Subject: [PATCH] =?UTF-8?q?[GWL-424]=20=EC=B9=BC=EB=A7=8C=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EB=B0=9C=EC=97=B4=20=EC=9D=B4=EC=8A=88=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95=20(#441)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 칼만필터 발열 이슈 수정 * feat: 시작 위치 변경 * delete: 중복 코드 삭제 * build: SwiftFormat 수정 * style: SwiftFormat에 맞게 코드 수정 * feat: CI수정 * feat: SwiftForamt수정 --- .github/workflows/iOS_CI.yml | 2 +- .../ViewController/HomeViewController.swift | 3 +- .../KalmanFilterUpdateRequireElement.swift | 7 +- .../Domain/UseCases/KalmanFilter.swift | 134 +++++++++--------- .../Domain/UseCases/KalmanUseCase.swift | 15 +- .../WorkoutRouteMapViewController.swift | 49 +++---- .../WorkoutRouteMapViewModel.swift | 9 +- .../DesignSystem/Sources/GWPageConrol.swift | 1 - 8 files changed, 95 insertions(+), 125 deletions(-) diff --git a/.github/workflows/iOS_CI.yml b/.github/workflows/iOS_CI.yml index 1e8e510b..7f4ccbb3 100644 --- a/.github/workflows/iOS_CI.yml +++ b/.github/workflows/iOS_CI.yml @@ -10,7 +10,7 @@ on: jobs: swift-format: if: contains(github.event.pull_request.labels.*.name, '📱 iOS') - runs-on: macos-13 + runs-on: macos-latest env: working-directory: ./iOS diff --git a/iOS/Projects/Features/Home/Sources/Presntaion/HomeScene/ViewController/HomeViewController.swift b/iOS/Projects/Features/Home/Sources/Presntaion/HomeScene/ViewController/HomeViewController.swift index 3a5f7f1a..1d07dcdc 100644 --- a/iOS/Projects/Features/Home/Sources/Presntaion/HomeScene/ViewController/HomeViewController.swift +++ b/iOS/Projects/Features/Home/Sources/Presntaion/HomeScene/ViewController/HomeViewController.swift @@ -194,8 +194,7 @@ private extension HomeViewController { extension HomeViewController: UICollectionViewDelegate { func collectionView(_: UICollectionView, willDisplay _: UICollectionViewCell, forItemAt indexPath: IndexPath) { - - /// 사용자가 아직 보지 않은 셀의 갯수 + // 사용자가 아직 보지 않은 셀의 갯수 let toShowCellCount = (feedCount - 1) - indexPath.row if toShowCellCount < 3 { fetchFeedPublisher.send() diff --git a/iOS/Projects/Features/Record/Sources/Domain/Entities/KalmanFilterUpdateRequireElement.swift b/iOS/Projects/Features/Record/Sources/Domain/Entities/KalmanFilterUpdateRequireElement.swift index 2db4e7c8..993e8631 100644 --- a/iOS/Projects/Features/Record/Sources/Domain/Entities/KalmanFilterUpdateRequireElement.swift +++ b/iOS/Projects/Features/Record/Sources/Domain/Entities/KalmanFilterUpdateRequireElement.swift @@ -6,13 +6,12 @@ // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. // +import CoreLocation import Foundation // MARK: - KalmanFilterUpdateRequireElement struct KalmanFilterUpdateRequireElement { - let longitude: Double - let latitude: Double - let prevSpeedAtLongitude: Double - let prevSpeedAtLatitude: Double + let prevCLLocation: CLLocation + let currentCLLocation: CLLocation } diff --git a/iOS/Projects/Features/Record/Sources/Domain/UseCases/KalmanFilter.swift b/iOS/Projects/Features/Record/Sources/Domain/UseCases/KalmanFilter.swift index 2dc3420d..ec722a7a 100644 --- a/iOS/Projects/Features/Record/Sources/Domain/UseCases/KalmanFilter.swift +++ b/iOS/Projects/Features/Record/Sources/Domain/UseCases/KalmanFilter.swift @@ -6,29 +6,26 @@ // Copyright © 2023 kr.codesquad.boostcamp8. All rights reserved. // +import CoreLocation import Foundation import Log +import simd struct KalmanFilter { /// 새로운 값을 입력하게 된다면, 에측을 통해서 값을 작성하게 되는 변수 입니다. - var x = MatrixOfTwoDimension([[]]) + var x = simd_double4() - /// 초기 오차 공분산 입니다. - /// 초기 값은 에러가 많기 떄문에 다음과 같이 크게 가져갔습니다. - private var p = MatrixOfTwoDimension([ - [500, 0, 0, 0], + private var p = simd_double4x4([ + [1, 0, 0, 0], [0, 1, 0, 0], - [0, 0, 500, 0], + [0, 0, 1, 0], [0, 0, 0, 1], ]) - // 사용자 경험을 통해 얻어진 값 입니다. (일단 대한민국 GPS환경이 좋다고 가정하여, - // 애플 오차의 1/2로 가져갔습니다.) - - private var q = MatrixOfTwoDimension([ - [0.000455, 0, 0, 0], + private var q = simd_double4x4([ + [0.00082, 0, 0, 0], [0, 0, 0, 0], - [0, 0, 0.000059, 0], + [0, 0, 0.00082, 0], [0, 0, 0, 0], ]) @@ -45,77 +42,86 @@ struct KalmanFilter { /// 1도 일 때 몇 km? = 지구 반지름 6371 * cos(37) * 1 (pi/180) = 85.18km /// 1º : 85180m = y : 10m /// y = 0.00011739 ~= 0.000117 - private var r = MatrixOfTwoDimension([ - [0.000899, 0], - [0, 0.000117], + private var r = simd_double2x2([ + [0.00082, 0], + [0, 0.00082], ]) - var prevHeadingValue: Double - var prevSpeedAtLatitude: Double = 0 - var prevSpeedAtLongitude: Double = 0 - /// 관계 식 입니다. - lazy var A = MatrixOfTwoDimension([ - [1, cos(prevHeadingValue) * prevSpeedAtLatitude, 0, 0], + lazy var A = simd_double4x4([ + [1, timeInterval * prevVelocity.lat, 0, 0], [0, 1, 0, 0], - [0, 0, 1, sin(prevHeadingValue) * prevSpeedAtLongitude], + [0, 0, 1, timeInterval * prevVelocity.long], [0, 0, 0, 1], ]) - /// 우리가 궁금한건 위도와 경도이기 때문에 필요한 부분만 기재했습니다. - private var H = MatrixOfTwoDimension([ + let H = simd_double4x2([ + [1, 0], + [0, 0], + [0, 1], + [0, 0], + ]) + + // 우리가 궁금한건 위도와 경도이기 때문에 필요한 부분만 기재했습니다. + + var prevTime: Date + + init(initLocation: CLLocation) { + x = .init(initLocation.coordinate.latitude, 0, initLocation.coordinate.longitude, 0) + prevTime = initLocation.timestamp + } + + var timeInterval: Double = 0.0 + + var prevLocation = CLLocation() + + let pIdentity = simd_double4x4([ [1, 0, 0, 0], + [0, 1, 0, 0], [0, 0, 1, 0], + [0, 0, 0, 1], ]) - init(initLongitude: Double, initLatitude: Double, headingValue: Double) { - x = .init([ - [initLatitude], - [0], - [initLongitude], - [0], - ]) - prevHeadingValue = headingValue - } + var prevVelocity: (lat: Double, long: Double) = (0, 0) - /// 사용자가 가르키는 방향을 업데이트 합니다. - mutating func update(heading: Double) { - prevHeadingValue = heading - } + mutating func update(currentLocation: CLLocation) { + let currentTime = currentLocation.timestamp + + let prevTimeInterval = prevTime.timeIntervalSince1970 + let currentTimeInterval = currentTime.timeIntervalSince1970 + + timeInterval = currentTimeInterval - prevTimeInterval + + let velocityLatitude = (prevLocation.coordinate.latitude - currentLocation.coordinate.latitude) + let velocityLongitude = (prevLocation.coordinate.longitude - currentLocation.coordinate.longitude) - /// Update합니다. - mutating func update(initLongitude: Double, initLatitude: Double, prevSpeedAtLatitude: Double, prevSpeedAtLongitude: Double) { - let mesure = MatrixOfTwoDimension( + prevVelocity = (velocityLatitude, velocityLongitude) + + prevLocation = currentLocation + prevTime = currentTime + + let mesure = simd_double2( [ - [initLatitude], - [initLongitude], + currentLocation.coordinate.latitude, + currentLocation.coordinate.longitude, ] ) - self.prevSpeedAtLatitude = prevSpeedAtLatitude - self.prevSpeedAtLongitude = prevSpeedAtLongitude - guard - let prediction = A.multiply(x), - let predictionErrorCovariance = A.multiply(p)?.multiply(A.transPose())?.add(q), - - let notInversed = H.multiply(predictionErrorCovariance)?.multiply(H.transPose())?.add(r), - let prevKalman = notInversed.invert(), - let kalman = predictionErrorCovariance.multiply(H.transPose())?.multiply(prevKalman), - - let tempValue = H.multiply(prediction), - let subTractedValue = mesure.sub(tempValue), - let multiedKalmanValue = kalman.multiply(subTractedValue), - let currentX = prediction.add(multiedKalmanValue), - - let tempPredictionErrorCovariance = kalman.multiply(H)?.multiply(predictionErrorCovariance), - let currentPredictionErrorCovariance = predictionErrorCovariance.sub(tempPredictionErrorCovariance) - else { - return - } + + let xp = A * x + let pp = A * p * A.transpose + q + + let temp = pp * H.transpose + let invert = (H * pp * H.transpose + r).inverse + let kalman = temp * invert + + let currentX = xp + kalman * (mesure - H * xp) + + let currentP = pp - kalman * H * pp.inverse x = currentX - p = currentPredictionErrorCovariance + p = currentP } var latestCensoredPosition: KalmanFilterCensored { - return .init(longitude: x.value[2][0], latitude: x.value[0][0]) + return .init(longitude: x[2], latitude: x[0]) } } diff --git a/iOS/Projects/Features/Record/Sources/Domain/UseCases/KalmanUseCase.swift b/iOS/Projects/Features/Record/Sources/Domain/UseCases/KalmanUseCase.swift index 9cbd19ee..165dbfd4 100644 --- a/iOS/Projects/Features/Record/Sources/Domain/UseCases/KalmanUseCase.swift +++ b/iOS/Projects/Features/Record/Sources/Domain/UseCases/KalmanUseCase.swift @@ -13,7 +13,6 @@ import Foundation protocol KalmanUseCaseRepresentable { func updateFilter(_ element: KalmanFilterUpdateRequireElement) -> KalmanFilterCensored? - func updateHeading(_ heading: Double) } // MARK: - KalmanUseCase @@ -27,21 +26,13 @@ final class KalmanUseCase { // MARK: KalmanUseCaseRepresentable extension KalmanUseCase: KalmanUseCaseRepresentable { - func updateHeading(_ heading: Double) { - filter?.update(heading: heading) - } - func updateFilter(_ element: KalmanFilterUpdateRequireElement) -> KalmanFilterCensored? { if filter == nil { - filter = .init(initLongitude: element.latitude, initLatitude: element.longitude, headingValue: 0) + let currentLocation = element.currentCLLocation + filter = .init(initLocation: currentLocation) return nil } - filter?.update( - initLongitude: element.longitude, - initLatitude: element.latitude, - prevSpeedAtLatitude: element.prevSpeedAtLatitude, - prevSpeedAtLongitude: element.prevSpeedAtLongitude - ) + filter?.update(currentLocation: element.currentCLLocation) return filter?.latestCensoredPosition } diff --git a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/RouteMapScene/WorkoutRouteMapViewController.swift b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/RouteMapScene/WorkoutRouteMapViewController.swift index 1b915661..63a63d61 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/RouteMapScene/WorkoutRouteMapViewController.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/RouteMapScene/WorkoutRouteMapViewController.swift @@ -35,13 +35,12 @@ final class WorkoutRouteMapViewController: UIViewController { private let viewModel: WorkoutRouteMapViewModelRepresentable /// 사용자 위치 추적 배열 - @Published private var locations: [CLLocation] = [] + @Published private var locations: [CLLocationCoordinate2D] = [] private let mapCaptureDataSubject: PassthroughSubject = .init() private let mapSnapshotterImageDataSubject: PassthroughSubject<[CLLocation], Never> = .init() private let kalmanFilterShouldUpdatePositionSubject: PassthroughSubject = .init() - private let kalmanFilterShouldUpdateHeadingSubject: PassthroughSubject = .init() private var subscriptions: Set = [] @@ -137,7 +136,6 @@ final class WorkoutRouteMapViewController: UIViewController { let input: WorkoutRouteMapViewModelInput = .init( filterShouldUpdatePositionPublisher: kalmanFilterShouldUpdatePositionSubject.eraseToAnyPublisher(), - filterShouldUpdateHeadingPublisher: kalmanFilterShouldUpdateHeadingSubject.eraseToAnyPublisher(), locationListPublisher: locationPublisher ) @@ -167,8 +165,7 @@ final class WorkoutRouteMapViewController: UIViewController { return } - let coordinates = locations.map(\.coordinate) - let polyLine = MKPolyline(coordinates: coordinates, count: coordinates.count) + let polyLine = MKPolyline(coordinates: locations, count: locations.count) let span = MKCoordinateSpan( latitudeDelta: (regionData.maxLatitude - regionData.minLatitude) * 1.15, longitudeDelta: (regionData.maxLongitude - regionData.minLongitude) * 1.15 @@ -209,14 +206,10 @@ final class WorkoutRouteMapViewController: UIViewController { self?.locations .forEach { location in - let currentCLLocationCoordinator2D = CLLocationCoordinate2D( - latitude: location.coordinate.latitude, - longitude: location.coordinate.longitude - ) // snapshot에서 현재 위도 경도에 대한 데이터가 어느 CGPoint에 있는지 찾아내고, 이를 Polyline을 그립니다. - context.cgContext.addLine(to: snapshot.point(for: currentCLLocationCoordinator2D)) - context.cgContext.move(to: snapshot.point(for: currentCLLocationCoordinator2D)) + context.cgContext.addLine(to: snapshot.point(for: location)) + context.cgContext.move(to: snapshot.point(for: location)) } // 현재 컨텍스트 에서 여태 그린 Path를 적용합니다. @@ -232,9 +225,8 @@ final class WorkoutRouteMapViewController: UIViewController { } let currentLocation = CLLocation(latitude: value.latitude, longitude: value.longitude) - locations.append(currentLocation) - let coordinates = locations.map(\.coordinate) - let polyline = MKPolyline(coordinates: coordinates, count: coordinates.count) + locations.append(currentLocation.coordinate) + let polyline = MKPolyline(coordinates: locations, count: locations.count) mapView.removeOverlays(mapView.overlays) mapView.addOverlay(polyline) @@ -260,6 +252,7 @@ final class WorkoutRouteMapViewController: UIViewController { extension WorkoutRouteMapViewController: LocationTrackingProtocol { func requestCapture() { + let locations = locations.map { CLLocation(latitude: $0.latitude, longitude: $0.longitude) } mapSnapshotterImageDataSubject.send(locations) } @@ -268,7 +261,12 @@ extension WorkoutRouteMapViewController: LocationTrackingProtocol { } var locationPublisher: AnyPublisher<[CLLocation], Never> { - $locations.eraseToAnyPublisher() + return Just( + locations.map { + CLLocation(latitude: $0.latitude, longitude: $0.longitude) + } + ) + .eraseToAnyPublisher() } } @@ -293,28 +291,13 @@ extension WorkoutRouteMapViewController: CLLocationManagerDelegate { return } - let currentTime = Date.now - let timeDistance = currentTime.distance(to: prevDate) - - // 과거 위치와 현재 위치를 통해 위 경도에 관한 속력을 구합니다. - let v = ( - (newLocation.coordinate.latitude - prevLocation.coordinate.latitude) / timeDistance, - (newLocation.coordinate.longitude - prevLocation.coordinate.longitude) / timeDistance - ) - prevLocation = newLocation - kalmanFilterShouldUpdatePositionSubject.send( .init( - longitude: newLocation.coordinate.longitude, - latitude: newLocation.coordinate.latitude, - prevSpeedAtLongitude: v.1, - prevSpeedAtLatitude: v.0 + prevCLLocation: prevLocation, + currentCLLocation: newLocation ) ) - } - - func locationManager(_: CLLocationManager, didUpdateHeading newHeading: CLHeading) { - kalmanFilterShouldUpdateHeadingSubject.send(newHeading.trueHeading) + prevLocation = newLocation } func locationManager(_: CLLocationManager, didFailWithError error: Error) { diff --git a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/RouteMapScene/WorkoutRouteMapViewModel.swift b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/RouteMapScene/WorkoutRouteMapViewModel.swift index 0816b98e..9eeaead8 100644 --- a/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/RouteMapScene/WorkoutRouteMapViewModel.swift +++ b/iOS/Projects/Features/Record/Sources/Presentation/WorkoutSessionGroupScene/RouteMapScene/WorkoutRouteMapViewModel.swift @@ -13,7 +13,6 @@ import Foundation public struct WorkoutRouteMapViewModelInput { let filterShouldUpdatePositionPublisher: AnyPublisher - let filterShouldUpdateHeadingPublisher: AnyPublisher let locationListPublisher: AnyPublisher<[LocationModel], Never> } @@ -58,13 +57,6 @@ extension WorkoutRouteMapViewModel: WorkoutRouteMapViewModelRepresentable { public func transform(input: WorkoutRouteMapViewModelInput) -> WorkoutRouteMapViewModelOutput { subscriptions.removeAll() - input - .filterShouldUpdateHeadingPublisher - .sink { [kalmanUseCase] value in - kalmanUseCase.updateHeading(value) - } - .store(in: &subscriptions) - let region = input .locationListPublisher .map(locationPathUseCase.processPath(locations:)) @@ -73,6 +65,7 @@ extension WorkoutRouteMapViewModel: WorkoutRouteMapViewModelRepresentable { let updateValue: WorkoutRouteMapViewModelOutput = input .filterShouldUpdatePositionPublisher .dropFirst(4) + .throttle(for: 1, scheduler: RunLoop.main, latest: false) .map { [kalmanUseCase] element in let censoredValue = kalmanUseCase.updateFilter(element) return WorkoutRouteMapState.censoredValue(censoredValue) diff --git a/iOS/Projects/Shared/DesignSystem/Sources/GWPageConrol.swift b/iOS/Projects/Shared/DesignSystem/Sources/GWPageConrol.swift index 6264deb1..37efc007 100644 --- a/iOS/Projects/Shared/DesignSystem/Sources/GWPageConrol.swift +++ b/iOS/Projects/Shared/DesignSystem/Sources/GWPageConrol.swift @@ -62,7 +62,6 @@ private extension GWPageControl { var targetSpacing: CGFloat = 0 pages.forEach { page in - // 중요: 맨 처음 Page객체는 왼쪽으로 붙여야 하기에 필수 불가결적으로 다음 로직이 필요합니다. if targetLeadingAnchor != safeAreaLayoutGuide.leadingAnchor { targetSpacing = spacing }