반응형
서론
휴식맞쥬는 NaverMapSDK와 Directions5API를 통해 목적지 까지 경로선을 그리고, 경로선 위에 존재하는 모든 휴게소의 정보를 좌표를 바탕으로 보여주고 있습니다!
휴게소의 정보를 서버로 부터 받아오기 위해서 클라이언트단에서는 각 고속도로 경로의 정보를 추출해서 서버로 보내주어야합니다.
그.래.서 Directions5API를 통해 고속도로 경로 정보를 추출하는 코드와 더불어 제가 느꼈던걸 정리할려고합니다!
*별도의 Directions5API를 통해 내려오는 데이터 구조에 대해서는 설명을 안하겠습니다. 데이터 구조가 궁금하시면, 링크를 걸어 두었으니 들어가서 참고하시면 이해에 더욱 도움이 되실 수 있을겁니다.
문제 정의
경로추출의 주요 목표는 다음과 같습니다
- 경로 데이터 분석: 전체 경로에서 고속도로 섹션을 식별하고 추출.
- 고속도로 진입 포인트 식별: 경로 중 고속도로에 진입하는 첫 번째 포인트를 정확히 파악.
- 고속도로 섹션의 정확한 포함: 첫 번째 진입 포인트부터 고속도로 섹션이 시작되도록 보장.
- 서버 호환성: 서버에서 요구하는 형식으로 데이터를 전송하기 위해 고속도로 섹션을 쿼드(네 개의 좌표)로 나누기.
경로 데이터 분석
먼저, Route 구조체 내의 trafast 데이터를 활용하여 경로를 분석했습니다.
guide 배열에는 다양한 이동 지침(type)이 포함되어 있었고, 이 중 고속도로 진입에 해당하는 특정 type 코드를 기반으로 섹션을 필터링했습니다. 제가 구분한 type은 고속도로 진입에 연관된 모든 type입니당
let highwayEntranceTypes: Set<Int> = [50, 52, 54, 57, 59, 66, 68, 75, 76, 77, 121]
- highwayEntranceTypes는 고속도로 진입에 해당하는 type 코드들을 집합으로 정의한 것입니다.
- 이 집합을 통해 경로 가이드 중 고속도로 진입을 나타내는 가이드를 쉽게 식별할 수 있습니다.
첫 번째 고속도로 진입점 찾기
guard let firstEntranceGuide = guides.first(where: { highwayEntranceTypes.contains($0.type) }) else {
log.warning("경로에 고속도로 진입 가이드가 없습니다.")
return highwaySections
}
- guides.first(where:)를 사용하여 highwayEntranceTypes에 포함된 첫 번째 type을 가진 가이드를 찾습니다.
- 찾은 가이드의 pointIndex는 고속도로 진입 포인트를 나타냅니다.
- 이 인덱스를 통해 경로 내에서 고속도로 섹션을 추출합니다
섹션 필터링 및 인덱스 조정
for section in sections {
let sectionStartIndex = section.pointIndex
let sectionEndIndex = section.pointIndex + section.pointCount - 1
// 섹션이 firstEntrancePointIndex를 포함하는지 확인
if sectionEndIndex < firstEntrancePointIndex {
// 섹션이 firstEntrancePointIndex 이전에 끝나면 건너뜀
continue
}
// 섹션의 시작 인덱스 조정
let startIndex = max(sectionStartIndex, firstEntrancePointIndex)
let endIndex = sectionEndIndex
// 유효한 인덱스인지 확인
if startIndex < 0 || endIndex >= path.count || startIndex > endIndex {
log.warning("고속도로 섹션의 인덱스가 유효하지 않습니다. StartIndex: \(startIndex), EndIndex: \(endIndex), Path Count: \(path.count)")
continue
}
// 해당 섹션의 경로 추출 (인덱스와 함께)
let highwaySectionPathWithIndices = Array(zip(startIndex...endIndex, path[startIndex...endIndex]))
// 섹션을 여러 개의 쿼드로 변환
let quadsWithIndices = convertSectionToQuadsWithIndices(highwaySectionPathWithIndices, segmentLength: 10)
// 딕셔너리에 추가 (고속도로 키 사용)
highwaySections[highwayKey, default: []].append(contentsOf: quadsWithIndices)
log.info("고속도로 섹션 추가: 포인트 수: \(highwaySectionPathWithIndices.count)")
}
- 섹션 순회:
- 모든 섹션을 순회하면서 각 섹션이 고속도로 진입 포인트를 포함하는지 확인합니다.
- 섹션 포함 여부 확인:
- 섹션의 종료 인덱스(sectionEndIndex)가 firstEntrancePointIndex보다 작으면 해당 섹션은 고속도로 진입 이전이므로 건너뜁니다.
- 인덱스 조정:
- 섹션의 시작 인덱스(sectionStartIndex)가 firstEntrancePointIndex보다 작을 경우, firstEntrancePointIndex로 조정하여 고속도로 진입 포인트가 섹션에 포함되도록 합니다.
- 이는 섹션의 시작 인덱스가 고속도로 진입 이후인 경우, 해당 섹션을 그대로 사용하고, 그렇지 않은 경우 시작 인덱스를 조정하여 정확한 고속도로 섹션을 확보합니다.
- 유효성 검사:
- 조정된 startIndex와 endIndex가 유효한지 확인합니다.
- 유효하지 않은 인덱스는 로그로 경고를 남기고, 해당 섹션을 건너뜁니다.
- 경로 추출:
- 유효한 인덱스 범위 내에서 경로를 추출합니다.
- zip 함수를 사용하여 인덱스와 좌표를 함께 추출하여 highwaySectionPathWithIndices 배열에 저장합니다.
- 쿼드(4개로 이루어진 배열, 위치의 꼭짓점) 변환 및 추가:
- 추출된 경로를 convertSectionToQuadsWithIndices 함수를 통해 쿼드로 변환합니다.
- 변환된 쿼드를 highwaySections 딕셔너리에 추가합니다.
- 로그를 통해 추가된 섹션의 포인트 수를 확인할 수 있습니다.
쿼드로 변환
private func convertSectionToQuadsWithIndices(_ pathWithIndices: [(Int, [Double])], segmentLength: Int = 10) -> [([Int], [[Double]])] {
var quadsWithIndices: [([Int], [[Double]])] = []
let totalPoints = pathWithIndices.count
// 일정 길이(segmentLength)로 분할하여 쿼드 생성
let indices = stride(from: 0, to: totalPoints, by: segmentLength)
for startIndex in indices {
let endIndex = min(startIndex + segmentLength - 1, totalPoints - 1)
let segmentWithIndices = Array(pathWithIndices[startIndex...endIndex])
if let quadWithIndices = createQuadWithIndices(for: segmentWithIndices) {
quadsWithIndices.append(quadWithIndices)
}
}
return quadsWithIndices
}
- 쿼드 분할 기준:
- 쿼드를 나누는 이유: 서버에서는 고속도로 섹션을 4개의 좌표 배열로 받기 원하기 때문에, 경로 데이터를 쿼드(4개의 좌표로 이루어진 사각형)로 분할해야 합니다.
- 쿼드를 나누는 기준: 고속도로 진입/출 정보를 기반으로 섹션을 나누며, 일정한 길이(segmentLength)로 분할하여 서버에 맞는 형식으로 데이터를 전송합니다.
- 쪼개기 로직:
- stride(from:to:by:) 함수를 사용하여 pathWithIndices 배열을 segmentLength 단위로 분할합니다.
- 각 분할된 세그먼트는 최대 segmentLength 개의 포인트를 포함하게 됩니다.
- 마지막 세그먼트는 전체 포인트 수를 초과하지 않도록 min 함수를 사용하여 인덱스를 조정합니다.
- 쿼드 생성:
- 각 세그먼트를 createQuadWithIndices 함수로 전달하여 쿼드를 생성합니다.
- 생성된 쿼드는 quadsWithIndices 배열에 추가됩니다.
쿼드를 나누는 기준의 선택: segmentLength
segmentLength는 하나의 쿼드에 포함될 포인트의 수를 결정하는 중요한 파라미터입니다.
적절한 segmentLength를 선택하는 것은 데이터의 정확성과 서버 성능 간의 균형을 맞추는 데 필수적입니다.
선택 기준:
- 서버 요구사항:
- 서버에서 요구하는 데이터 형식과 크기를 고려하여 segmentLength를 설정해야 합니다. 예를 들어, 서버가 한 번에 처리할 수 있는 데이터 양을 초과하지 않도록 주의해야 합니다.
- 데이터 밀도:
- 경로 데이터의 포인트 밀도가 높은 경우, segmentLength를 작게 설정하여 더 많은 쿼드로 분할할 수 있습니다. 반대로 포인트 밀도가 낮으면 segmentLength를 크게 설정하여 쿼드의 수를 줄일 수 있습니다.
- 성능 최적화:
- segmentLength가 너무 크면 각 쿼드가 지나치게 많은 포인트를 포함하게 되어 서버 처리 시간이 늘어날 수 있습니다. 반대로 너무 작으면 쿼드의 수가 증가하여 관리가 복잡해질 수 있습니다.
- 일반적으로 segmentLength는 10~100 사이에서 선택하며, 프로젝트의 특성과 서버의 성능에 맞춰 조정합니다.
- 지도 시각화 요구사항:
- 지도에 데이터를 시각화할 때, 쿼드의 크기와 분할 기준이 시각적인 명확성을 유지하는 데 영향을 미칩니다. 적절한 segmentLength는 지도에서 고속도로 섹션이 명확하게 구분되도록 도와줍니다.
예시:
- segmentLength = 10:
- 고속도로 섹션을 10개의 포인트로 나누어 쿼드를 생성합니다.
- 각 쿼드는 비교적 작은 영역을 커버하여 세밀한 데이터 처리가 가능합니다.
- 서버의 데이터 처리 능력이 뛰어나고, 데이터의 정확성이 중요한 경우 적합합니다.
- segmentLength = 50:
- 고속도로 섹션을 50개의 포인트로 나누어 쿼드를 생성합니다.
- 각 쿼드는 더 넓은 영역을 커버하여 데이터의 양을 줄일 수 있습니다.
- 서버의 처리 능력이 제한적이거나, 데이터의 대략적인 경로만 필요한 경우 적합합니다.
쿼드 생성
private func createQuadWithIndices(for segmentWithIndices: [(Int, [Double])]) -> ([Int], [[Double]])? {
guard segmentWithIndices.count >= 2 else { return nil }
let indices = segmentWithIndices.map { $0.0 }
let points = segmentWithIndices.map { $0.1 }
let latitudes = points.map { $0[1] }
let longitudes = points.map { $0[0] }
guard let minLat = latitudes.min(),
let maxLat = latitudes.max(),
let minLon = longitudes.min(),
let maxLon = longitudes.max() else {
return nil
}
let quad = [
[minLon, maxLat], // 좌상단
[maxLon, maxLat], // 우상단
[maxLon, minLat], // 우하단
[minLon, minLat] // 좌하단
]
return (indices, quad)
}
- 입력 검증:
- 최소 두 개 이상의 포인트가 있는지 확인합니다. 쿼드를 생성하기 위해서는 최소 두 개의 포인트가 필요합니다. 포인트가 부족하면 nil을 반환하여 쿼드 생성을 생략합니다.
- 좌표 추출:
- 각 포인트의 인덱스와 좌표를 별도로 추출합니다.
- indices는 쿼드에 포함된 포인트들의 인덱스 배열입니다.
- points는 각 포인트의 [경도, 위도] 배열입니다.
- latitudes와 longitudes는 각각 위도와 경도를 별도로 추출한 배열입니다.
- 최소 및 최대 위도, 경도 계산:
- minLat, maxLat는 섹션 내에서의 최소 및 최대 위도 값입니다.
- minLon, maxLon는 섹션 내에서의 최소 및 최대 경도 값입니다.
- 이 값들을 사용하여 쿼드의 경계 좌표를 정의합니다.
- 쿼드 생성:
- 각 쿼드는 좌상단, 우상단, 우하단, 좌하단의 네 개의 좌표로 이루어진 사각형입니다.
- [minLon, maxLat]는 좌상단, [maxLon, maxLat]는 우상단, [maxLon, minLat]는 우하단, [minLon, minLat]는 좌하단을 나타냅니다.
- 이러한 사각형은 고속도로 섹션을 시각화하거나, 서버에서 효율적으로 처리할 수 있는 형태로 데이터를 제공합니다.
- 쿼드 및 인덱스 반환:
- 생성된 쿼드와 해당 세그먼트의 인덱스를 튜플 형태로 반환합니다.
- 이 정보는 서버에서 요구하는 형식에 맞추어 데이터를 전송하는 데 사용됩니다.
전체 코드 및 요약
extension LocationInfoUseCase {
/// 고속도로 경로 추출하는 함수 (정사각형의 4개의 좌표배열)
func extractHighwaySections(from route: Route) -> [String: [([Int], [[Double]])]] {
var highwaySections: [String: [([Int], [[Double]])]] = [:]
let highwayKey = "고속도로"
// Trafast 데이터가 존재하는지 확인
guard let trafast = route.trafast.first else {
log.warning("경로에 trafast 데이터가 없습니다.")
return highwaySections
}
let path = trafast.path
let sections = trafast.section
let guides = trafast.guide
// 고속도로 진입 타입 정의 (고속도로 진입에 해당하는 타입만 포함)
let highwayEntranceTypes: Set<Int> = [50, 52, 54, 57, 59, 66, 68, 75, 76, 77, 121]
// 첫 번째 고속도로 진입 가이드 찾기
guard let firstEntranceGuide = guides.first(where: { highwayEntranceTypes.contains($0.type) }) else {
log.warning("경로에 고속도로 진입 가이드가 없습니다.")
return highwaySections
}
let firstEntrancePointIndex = firstEntranceGuide.pointIndex
log.debug("첫 번째 고속도로 진입 포인트 인덱스: \(firstEntrancePointIndex), 좌표: \(path[firstEntrancePointIndex])")
// 모든 섹션을 순회하며 고속도로 섹션 추출
for section in sections {
let sectionStartIndex = section.pointIndex
let sectionEndIndex = section.pointIndex + section.pointCount - 1
// 섹션이 firstEntrancePointIndex를 포함하는지 확인
if sectionEndIndex < firstEntrancePointIndex {
// 섹션이 firstEntrancePointIndex 이전에 끝나면 건너뜀
continue
}
// 섹션의 시작 인덱스 조정
let startIndex = max(sectionStartIndex, firstEntrancePointIndex)
let endIndex = sectionEndIndex
// 유효한 인덱스인지 확인
if startIndex < 0 || endIndex >= path.count || startIndex > endIndex {
log.warning("고속도로 섹션의 인덱스가 유효하지 않습니다. StartIndex: \(startIndex), EndIndex: \(endIndex), Path Count: \(path.count)")
continue
}
// 해당 섹션의 경로 추출 (인덱스와 함께)
let highwaySectionPathWithIndices = Array(zip(startIndex...endIndex, path[startIndex...endIndex]))
// 섹션을 여러 개의 쿼드로 변환
let quadsWithIndices = convertSectionToQuadsWithIndices(highwaySectionPathWithIndices, segmentLength: 10)
// 딕셔너리에 추가 (고속도로 키 사용)
highwaySections[highwayKey, default: []].append(contentsOf: quadsWithIndices)
log.info("고속도로 섹션 추가: 포인트 수: \(highwaySectionPathWithIndices.count)")
}
return highwaySections
}
///섹션을 쿼드로 변환하는 함수 (인덱스 포함)
private func convertSectionToQuadsWithIndices(_ pathWithIndices: [(Int, [Double])], segmentLength: Int = 10) -> [([Int], [[Double]])] {
var quadsWithIndices: [([Int], [[Double]])] = []
let totalPoints = pathWithIndices.count
// 일정 길이(segmentLength)로 분할하여 쿼드 생성
let indices = stride(from: 0, to: totalPoints, by: segmentLength)
for startIndex in indices {
let endIndex = min(startIndex + segmentLength - 1, totalPoints - 1)
let segmentWithIndices = Array(pathWithIndices[startIndex...endIndex])
if let quadWithIndices = createQuadWithIndices(for: segmentWithIndices) {
quadsWithIndices.append(quadWithIndices)
}
}
return quadsWithIndices
}
/// 주어진 구간에서 쿼드를 생성하는 함수 (인덱스 포함)
private func createQuadWithIndices(for segmentWithIndices: [(Int, [Double])]) -> ([Int], [[Double]])? {
guard segmentWithIndices.count >= 2 else { return nil }
let indices = segmentWithIndices.map { $0.0 }
let points = segmentWithIndices.map { $0.1 }
let latitudes = points.map { $0[1] }
let longitudes = points.map { $0[0] }
guard let minLat = latitudes.min(),
let maxLat = latitudes.max(),
let minLon = longitudes.min(),
let maxLon = longitudes.max() else {
return nil
}
let quad = [
[minLon, maxLat], // 좌상단
[maxLon, maxLat], // 우상단
[maxLon, minLat], // 우하단
[minLon, minLat] // 좌하단
]
return (indices, quad)
}
}
- extractHighwaySections 함수:
- 목적: 주어진 Route에서 고속도로 섹션을 추출하여, 서버에서 요구하는 형식(4개의 좌표 배열)으로 데이터를 전송하기 위해 쿼드로 분할합니다.
- 과정:
- trafast 데이터가 존재하는지 확인.
- 고속도로 진입에 해당하는 type 코드를 정의.
- 첫 번째 고속도로 진입 가이드를 찾아 해당 포인트 인덱스를 식별.
- 모든 섹션을 순회하며 고속도로 진입 이후의 섹션을 필터링.
- 필터링된 섹션에서 경로를 추출하고, 이를 쿼드로 변환.
- 변환된 쿼드를 highwaySections 딕셔너리에 추가.
- convertSectionToQuadsWithIndices 함수:
- 목적: 추출된 고속도로 섹션을 일정한 길이(segmentLength)로 분할하여 쿼드로 변환합니다.
- 과정:
- pathWithIndices 배열을 segmentLength 단위로 분할.
- 각 세그먼트를 createQuadWithIndices 함수로 전달하여 쿼드 생성.
- 생성된 쿼드를 quadsWithIndices 배열에 추가.
- createQuadWithIndices 함수:
- 목적: 주어진 세그먼트의 포인트들을 바탕으로 쿼드를 생성합니다.
- 과정:
- 최소 두 개 이상의 포인트가 있는지 확인.
- 각 포인트의 위도와 경도를 추출하여 최소 및 최대 값을 계산.
- 최소/최대 위도와 경도를 사용하여 사각형의 네 개의 좌표를 정의.
- 정의된 쿼드와 해당 세그먼트의 인덱스를 반환.
결론 및 전체 흐름 요약
경로 데이터에서 특정 섹션을 추출하는 과정을 경험할 수 있었습니다. 초기에는 고속도로 진입 포인트가 고속도로 섹션에 포함되지 않는 문제가 있었지만, 섹션의 시작 인덱스를 조정하는 로직을 개선함으로써 문제를 해결할 수 있었습니다.
특히, 고속도로 섹션을 쿼드로 나누는 과정은 서버와의 데이터 호환성을 위해 필수적이었습니다. 서버는 고속도로 섹션을 4개의 좌표 배열로 받기 원하기 때문에, 이를 충족시키기 위해 쿼드로 데이터를 분할하는 로직을 구현했습니다. 이 과정에서 고속도로 진입/출 정보를 기반으로 경로를 정확히 분할하여, 데이터의 일관성과 정확성을 유지할 수 있었습니다.
또한, segmentLength의 적절한 설정을 통해 데이터의 정확성과 서버 성능 간의 균형을 맞출 수 있었습니다. 이 과정에서 데이터 구조를 정확히 이해하고, 각 섹션의 인덱스와 포인트 간의 관계를 명확히 파악하는 것이 중요했습니다. 디버깅을 통해 문제의 원인을 분석하고, 이를 해결하기 위한 적절한 로직 수정을 통해 원하는 결과를 얻을 수 있었습니다.
향후에는 이 코드를 더욱 최적화하고, 다양한 경로 데이터에 대응할 수 있도록 확장하는 작업을 진행할 예정입니다.
사실 해당 로직전에 구현한 로직에 관해서도 함께 작성할려하였지만 글이 너무 길어지는것 같아(사실 더 이상 머리가 안돌아갑니다.. 데구르르) 2편에 이에 대한 내용을 적도록 하겠습니다.
반응형
'프로젝트 및 개발적 고민 > Project' 카테고리의 다른 글
[Swift/휴식맞쥬] DIContainer (2) | 2024.09.03 |
---|