Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[iOS] 여정 드로잉에 필요한 filter를 평균값을 활용하도록 수정 #315

Merged
merged 14 commits into from
Jan 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,9 @@ public final class HomeViewController: HomeBottomSheetViewController {
super.viewDidAppear(animated)

// 화면 시작 시 새로고침 버튼 기능 한번 실행
self.refreshButton.sendActions(for: .touchUpInside)
if !self.viewModel.state.isRecording.value {
self.refreshButton.sendActions(for: .touchUpInside)
}
}

// MARK: - Combine Binding
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,15 @@ public final class MapViewController: UIViewController {
self?.drawPolyline(using: points)
}
.store(in: &self.cancellables)

viewModel.state.filteredCoordinate
.receive(on: DispatchQueue.main)
.sink { coordinate in
guard let filteredCoordinate2D = coordinate else { return }
viewModel.trigger(.locationDidUpdated(filteredCoordinate2D))
viewModel.trigger(.locationsShouldRecorded([filteredCoordinate2D]))
}
.store(in: &self.cancellables)
}

// MARK: - Functions: Annotation
Expand Down Expand Up @@ -347,9 +356,7 @@ extension MapViewController: CLLocationManagerDelegate {

let coordinate2D = CLLocationCoordinate2D(latitude: newCurrentLocation.coordinate.latitude,
longitude: newCurrentLocation.coordinate.longitude)

recordJourneyViewModel.trigger(.locationDidUpdated(coordinate2D))
recordJourneyViewModel.trigger(.locationsShouldRecorded([coordinate2D]))
recordJourneyViewModel.trigger(.tenLocationsDidRecorded(coordinate2D))
}

private func handleAuthorizationChange(_ manager: CLLocationManager) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,16 @@ public final class RecordJourneyViewModel: MapViewModel {
public enum Action {
case locationDidUpdated(CLLocationCoordinate2D)
case locationsShouldRecorded([CLLocationCoordinate2D])
case tenLocationsDidRecorded(CLLocationCoordinate2D)
}

public struct State {
// CurrentValue
public var previousCoordinate = CurrentValueSubject<CLLocationCoordinate2D?, Never>(nil)
public var currentCoordinate = CurrentValueSubject<CLLocationCoordinate2D?, Never>(nil)
public var recordingJourney: CurrentValueSubject<RecordingJourney, Never>
public var recordedCoordinates = CurrentValueSubject<[CLLocationCoordinate2D], Never>([])
public var filteredCoordinate = PassthroughSubject<CLLocationCoordinate2D?, Never>()
}

// MARK: - Properties
Expand Down Expand Up @@ -51,20 +54,157 @@ public final class RecordJourneyViewModel: MapViewModel {
let previousCoordinate = self.state.currentCoordinate.value
self.state.previousCoordinate.send(previousCoordinate)
self.state.currentCoordinate.send(coordinate)

/// 저장하고자 하는 위치 데이터를 서버에 전송
case .locationsShouldRecorded(let coordinates):
Task {
let recordingJourney = self.state.recordingJourney.value
let coordinates = coordinates.map { Coordinate(latitude: $0.latitude, longitude: $0.longitude) }
let coordinates = coordinates.map { Coordinate(latitude: $0.latitude,
longitude: $0.longitude) }
let result = await self.journeyRepository.recordJourney(journeyID: recordingJourney.id,
at: coordinates)
switch result {
case .success(let recordingJourney):
self.state.recordingJourney.send(recordingJourney)
self.state.filteredCoordinate.send(nil)
case .failure(let error):
MSLogger.make(category: .home).error("\(error)")
}
}
/// 업데이트되는 위치 정보를 받아와 10개가 될 경우 필터링 후 저장하는 로직
case .tenLocationsDidRecorded(let coordinate):
self.filterCoordinate(coordinate: coordinate)
}
}

}

private extension RecordJourneyViewModel {

func calculateDistance(from coordinate1: CLLocationCoordinate2D,
to coordinate2: CLLocationCoordinate2D) -> CLLocationDistance {
let location1 = CLLocation(latitude: coordinate1.latitude,
longitude: coordinate1.longitude)
let location2 = CLLocation(latitude: coordinate2.latitude,
longitude: coordinate2.longitude)
return location1.distance(from: location2)
}

/// 배열의 중앙값을 도출
func findMedianFrom(array: [CLLocationDegrees]) -> CLLocationDegrees {
let sortedArray = array.sorted()
let count = sortedArray.count

if count % 2 == 0 {
// Array has even number of elements
let middle1 = sortedArray[count / 2 - 1]
let middle2 = sortedArray[count / 2]
return (middle1 + middle2) / 2.0
} else {
// Array has odd number of elements
return sortedArray[count / 2]
}
}

/// 위도 배열, 경도 배열로부터 중앙값을 도출
func medianCoordinate(recordedCoordinates: [CLLocationCoordinate2D]) -> CLLocationCoordinate2D {

// 10개의 위도 값 배열 생성
let latitudes = recordedCoordinates.map { coord in
coord.latitude
}.sorted()

// 10개의 경도 값 배열 생성
let longitudes = recordedCoordinates.map { coord in
coord.longitude
}.sorted()

return CLLocationCoordinate2D(latitude: findMedianFrom(array: latitudes),
longitude: findMedianFrom(array: longitudes))
}

/// 위도 배열, 경도 배열로부터 평균값을 도출
func averageCoordinate(recordedCoordinates: [CLLocationCoordinate2D]) -> CLLocationCoordinate2D {
let recordedLength = recordedCoordinates.count
let initialCoordinate = recordedCoordinates.reduce(CLLocationCoordinate2D(latitude: 0,
longitude: 0)) { result, coordinate in
return CLLocationCoordinate2D(latitude: result.latitude + coordinate.latitude,
longitude: result.longitude + coordinate.longitude)
}
let initialLat = initialCoordinate.latitude
let initialLong = initialCoordinate.longitude
return CLLocationCoordinate2D(latitude: initialLat / Double(recordedLength),
longitude: initialLong / Double(recordedLength))
}

/// 두 위치의 거리를 구하기
func distanceWith(_ coordinate1: CLLocationCoordinate2D,
_ coordinate2: CLLocationCoordinate2D) -> Double {
let location1 = CLLocation(latitude: coordinate1.latitude,
longitude: coordinate1.longitude)
let location2 = CLLocation(latitude: coordinate2.latitude,
longitude: coordinate2.longitude)

return location1.distance(from: location2)
}

/// 현재 location들 중 평균으로부터 가장 먼 지점을 제거
func deleteFarLocation(recordedCoordinates: [CLLocationCoordinate2D],
average: CLLocationCoordinate2D) -> [CLLocationCoordinate2D] {
var coordinates = recordedCoordinates
var maxDistance = 0.0
var maxDistanceIndex = -1
for (index, coordinate) in recordedCoordinates.enumerated() {
let location1 = CLLocation(latitude: coordinate.latitude,
longitude: coordinate.longitude)
let location2 = CLLocation(latitude: average.latitude,
longitude: average.longitude)
let distance = location1.distance(from: location2)
if distance > maxDistance {
maxDistance = distance
maxDistanceIndex = index
}
}
coordinates.remove(at: maxDistanceIndex)
return coordinates

}

func filterCoordinate(coordinate: CLLocationCoordinate2D) {
if let previousCoordinate = self.state.previousCoordinate.value,
calculateDistance(from: previousCoordinate,
to: coordinate) <= 5 {
return
}
var recordedCoords = self.state.recordedCoordinates.value
recordedCoords.append(coordinate)
self.state.recordedCoordinates.send(recordedCoords)

if self.state.recordedCoordinates.value.count >= 10 {

var finalAverage = CLLocationCoordinate2D()

while true {
let average = averageCoordinate(recordedCoordinates: recordedCoords)
let median = medianCoordinate(recordedCoordinates: recordedCoords)

if distanceWith(average,
median) <= 10 {
finalAverage = average
break
} else {
recordedCoords = deleteFarLocation(recordedCoordinates: recordedCoords,
average: average)
}

if recordedCoords.count == 1 {
finalAverage = recordedCoords[0]
break
}
}

self.state.filteredCoordinate.send(finalAverage)
self.state.recordedCoordinates.send([])
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,15 @@ public struct UserRepositoryImplementation: UserRepository {
let result = await self.networking.request(UserResponseDTO.self, router: router)
switch result {
case .success(let userResponse):
#if DEBUG
MSLogger.make(category: .network).debug("서버에 새로운 유저를 생성했습니다.")
#endif
if let storedUserID = try? self.storeUUID(userID) {
return .success(storedUserID)
} else {
MSLogger.make(category: .keychain).warning("Keychain에 새로운 유저 정보를 저장하지 못했습니다.")
return .success(userResponse.userID)
}
MSLogger.make(category: .keychain).warning("서버에 새로운 유저를 생성했지만, Keychain에 저장하지 못했습니다.")
return .success(userResponse.userID)
case .failure(let error):
return .failure(error)
}
Expand All @@ -66,6 +70,9 @@ public struct UserRepositoryImplementation: UserRepository {

do {
try self.keychain.set(value: userID, account: account)
#if DEBUG
MSLogger.make(category: .keychain).debug("Keychain에 서버 정보를 저장했습니다.")
#endif
return userID
} catch {
throw MSKeychainStorage.KeychainError.creationError
Expand Down