본문 바로가기
C.W.K.
Stream
Lesson 04 of 04 · published

동작은 단계의 끝에 있어

~14 min · swift, mapkit, navigation, conventions, map-camera

Level 0길 잃음
0 XP0/36 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete

아무도 안 적어 두는 관례

길찾기 제공자는 다 단계 목록을 돌려줘. 단계마다 뻔한 필드 두 개가 있어. 안내 문구, 그리고 거리. 문서에 잘 안 나오는 건 둘 사이의 관계야. "X로 우회전"은 그 거리의 시작에서 하는 거야, 끝에서 하는 거야? 두 해석 다 앞뒤가 맞아. 제공자마다 맞는 건 하나뿐이고, 잘못 짚으면 모든 회전이 엉뚱한 자리에서 안내돼.

Pathfinder는 짐작 안 했어. 쟀어. 태어난 첫날(2026-10-01), MapKit과 같은 데이터를 주는 Maps Server API로 Apple의 한국어 경로를 받아서 단계를 실제 선형이랑 맞춰 읽었어. "장항IC에서 … 왼쪽 차선 유지" 단계는 장항IC에서 끝나는 1.5km를 들고 있었고, 마지막 "목적지에 도착"은 남은 109m를 들고 있었어. 그다음 시뮬레이터에서 Apple 자기 경로 선형을 따라 주행을 다시 돌려서 MapKit 자체로도 확인했어. "백마로로 우회전"은 백마로로 실제로 꺾기 150m 전에 떴어. 그 단계의 150m는 회전까지 가는 길이었던 거야.

끌리는 대로 그 단계의 동작이 시작점에 있다고 읽어봐. 피파는 150m나 일찍 "우회전해"라고 말해. 그것도 엉뚱한 교차로에서. 나들목 단계였으면 오차가 1.5km야.

한 번 박아 둬: maneuverAlong

아키텍처 문서는 이 측정을 불변식 3번으로 만들었어. 단계의 안내 문구는 그 단계 끝의 동작이다. 코드는 이걸 잊을 수 없게 했어. NavStep에 답을 이미 담은 필드를 줬거든. maneuverAlong, 전체 경로에서 그 동작이 일어나는 지점까지의 거리야. 측정 사례는 그 타입의 문서 주석에 그대로 들어 있어. 고치러 온 사람이 증거부터 마주치게.

엔진은 언제 말할지 정할 때 단계의 distance를 안 봐. maneuverAlong - progress만 읽어. 그러니 제공자마다 자기 관례를 이 숫자 하나로 번역해야 하고, 그 번역은 안내 쪽으로 한 방울도 안 새.

  • Apple(AppleMapsProvider.navRoute): 단계 선형의 마지막 점을 잡아서, 이전 단계가 끝난 데서 시작하는 창 안에서 전체 경로에 투영해. 선에서 60m 안이면 받아들이고, 절대 뒤로 안 가고, 경로 길이를 넘지 않게 잘라. 투영이 실패하면 누적 거리로 대신해.
  • Kakao는 관례가 둘이고, 둘 다 cwkPippa가 폰에 닿기 전에 번역해. 운전 안내 지점은 그 안내가 말하는 도로의 첫 꼭짓점에 있어(실제 경로의 모든 안내에서 0.0m로 쟀어). 도보 단계는 구간 자체를 설명해. 그래서 시작점의 동작은 Kakao 문구에서 읽고, 문구에 없으면 선이 꺾이는 모양에서 읽어.
  • Google은 스스로 안내해. 그쪽 주행은 이 엔진을 아예 안 거치고, 동작은 자기 피드로 들어와.

시험 데이터도 같은 규칙을 따라. "동쪽 3km, 좌회전, 북쪽 2km, 도착"은 좌회전을 3,000m에, 도착을 5,000m에 두고, 맨 앞 단계인 말 없는 출발 자리표시자는 절대 말하지 않아.

지도는 아빠가 보는 쪽을 봐

같은 규율이 지도가 어디를 볼지에도 걸려 있어. 키트 안에서 값으로 한 번 정하고, Mac에서 시험해. 아빠 판정은 짧았어. 3D만 있으면 헷갈린다. 그래서 지도는 기본이 평면이고 3D는 버튼 하나 거리야. 진행 방향이 위고, 아빠는 화면 아래쪽에 있어. 카카오내비처럼.

FollowCamera는 두 질문에 답하는 순수한 값이야. 지도는 어느 쪽을 봐야 해? 운전이면, 2.5m/s 이상으로 움직이는 동안은 GPS 진행 방향, 아니면 지금 진행 지점에서 경로 위 도로 방향, 그것도 없으면 직전 방향. 걷기면, 1.8m/s로 씩씩하게 걸을 땐 진행 방향, 아니면 나침반. 사람은 제자리에서 돌고, 폰은 그걸 아니까. 기본값으로 북쪽은 절대 없어. 옛 코드는 북쪽으로 돌아가서 빨간불마다 지도를 빙글 돌렸거든. 카메라는 어디 둬야 해? 걷기는 350m. 운전은 속도 × 40m를 600m에서 1,800m 사이로 잘라서, 고속도로에선 앞길이 더 보이게. 중심은 2D에선 그 거리의 0.12배(기울인 화면에선 0.05배)만큼 아빠 앞으로 밀어. 시뮬레이터에서 재 보니 화면 높이에 카메라 거리의 0.55배쯤이 담겨. 그러면 아빠는 중심에서 22%쯤 아래에 와. 기울기는 평면 0도, 3D 55도.

주행이 경로 위에 있는 동안 아빠 표시는 스냅숏의 맞춘 지점, 그러니까 선에 붙인 위치에 놓여. 지도가 따라가는 화살표랑 목소리가 따지는 위치가 같은 점인 거야.

관례가 두 갈래로 읽히면, 한 번 재고 관례 말고 답을 저장해. 위치를 이미 담은 필드는 다음 사람이 잘못 읽을 수가 없어. 어느 쪽 끝을 쓰라고 설명하는 주석은 잘못 읽힐 수 있고.

Code

AppleMapsProvider.swift: 단계마다 끝점을 전체 경로에 박기 (원문 발췌)·swift
    static func navRoute(from route: MKRoute, origin: Coordinate, destination: Place) -> NavRoute {
      let polyline = Polyline(Self.coordinates(of: route.polyline))
      var steps: [NavStep] = []
      var along = 0.0
      for step in route.steps {
        // A step's instruction is the maneuver at the END of its path (see
        // `NavStep`); pin that end on the whole route, never moving backwards.
        let points = Self.coordinates(of: step.polyline)
        var maneuverAlong = along + step.distance
        if let end = points.last {
          let window = along...(along + step.distance * 1.5 + 200)
          if let match = polyline.project(end, within: window, preferAlong: along + step.distance),
            match.lateral < 60
          {
            maneuverAlong = max(along, match.distanceAlong)
          }
        }
        maneuverAlong = min(maneuverAlong, polyline.length)
        steps.append(
          NavStep(
            instruction: step.instructions, distance: step.distance, maneuverAlong: maneuverAlong,
            notice: step.notice))
        along = maneuverAlong
      }
최소 스케치: 같은 단계를 끝으로 읽을 때와 시작으로 읽을 때 (swiftc로 컴파일해 실행함)·swift
// A minimal sketch: two readings of the same provider steps. Each step is
// (instruction, metres driven on that step). The numbers echo the measured
// shape (1.5 km to an interchange, a short step to a turn, 109 m to arrive)
// but the route is made up.
let steps: [(instruction: String, distance: Double)] = [
  ("", 0),                               // the departure placeholder
  ("keep left at the interchange", 1500),
  ("turn right", 150),
  ("arrive", 109),
]

// Reading A (Pathfinder's NavStep.maneuverAlong): the maneuver happens at
// the END of the step's path, so its position is the running total.
// Reading B (the tempting one): the maneuver happens where the step starts.
var along = 0.0
var atEnd: [Double] = [], atStart: [Double] = []
for step in steps {
  atStart.append(along)
  along += step.distance
  atEnd.append(along)
}

for (i, step) in steps.enumerated() where !step.instruction.isEmpty {
  let early = atEnd[i] - atStart[i]
  print("\(step.instruction): end-reading \(Int(atEnd[i])) m, start-reading \(Int(atStart[i])) m (\(Int(early)) m early)")
}

// The car has just done the keep-left: 1500 m along.
let progress = 1500.0
let i = 2
print("at \(Int(progress)) m, '\(steps[i].instruction)' is \(Int(atEnd[i] - progress)) m ahead (A) or \(Int(atStart[i] - progress)) m ahead (B)")
// keep left at the interchange: end-reading 1500 m, start-reading 0 m (1500 m early)
// turn right: end-reading 1650 m, start-reading 1500 m (150 m early)
// arrive: end-reading 1759 m, start-reading 1650 m (109 m early)
// at 1500 m, 'turn right' is 150 m ahead (A) or 0 m ahead (B)
FollowCamera.swift: 지도가 어느 쪽을 볼지 (원문 그대로)·swift
  /// The heading the map should face.
  ///
  /// GPS course means something only while moving; stopped at a light it is
  /// noise or nothing, and the old code fell back to north, spinning the map.
  /// Driving: course when moving, else the road's direction on the route,
  /// else where the map already faced. Walking: course at a brisk pace, else
  /// the compass (a person turns on the spot; the phone knows), else the
  /// route, else the previous heading.
  public static func heading(
    mode: TravelMode, course: Double?, speed: Double?, compass: Double?, routeBearing: Double?, previous: Double?
  ) -> Double? {
    let moving = (speed ?? 0) >= (mode == .walking ? 1.8 : 2.5)
    if moving, let course { return Geo.normalized(course) }
    let order: [Double?] =
      mode == .walking ? [compass, routeBearing, previous] : [routeBearing, previous, compass]
    return order.compactMap { $0 }.first.map(Geo.normalized)
  }

External links

Exercise

호출할 수 있는 길찾기 API 하나를 골라봐. MapKit의 MKDirections, 공개 OSRM 서버, 아무 길찾기 API나. 확실한 회전 하나가 있는 경로를 요청해. 단계마다 선형이 어디서 시작하고 끝나는지 실제 회전 위치와 비교해서, 안내 문구가 어느 쪽 끝을 말하는지 판정해. 그 제공자의 단계를 (instruction, maneuverAlong) 쌍 목록으로 바꾸는 번역 함수를 짜고, 누가 관례를 뒤집으면 실패하는 시험도 하나 써.
Hint
원점은 공공 랜드마크나 지어낸 점으로 해. 집은 절대 안 돼. 제일 확실한 증거는 마지막 단계야. 'arrive' 같은 단계인데 거리가 0이 아니면, 그 문구는 그 거리의 끝을 말하는 거야.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.