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

아토차에서 생긴 일

~12 min · swift, navigation, walking, arrival, design

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

엉뚱한 길바닥에 도착한 도보 안내

아빠가 Pathfinder 안내를 받으면서 마드리드 Puerta de Atocha 역까지 걸어갔어. 목적지는 핀 하나였고, 핀은 핀이 늘 서는 자리에 서 있었어. 역 한가운데 점. Pathfinder는 도보 안내가 하는 일을 정확히 했어. 그 점까지 가는 도보 경로를 받고, 측위마다 따라가고, 회전마다 피파 목소리로 말했지. 경로는 출발 홀보다 두 층 아래 길바닥에서 끝났고, 거기서 "왼쪽으로 가"라고 했어. 출발 홀은 긴 계단 위에 있었고. 경로 기준으로 보면 실패한 건 없어. 목적지에 닿았으니까. 틀린 건 목적지였어.

이걸 고친 커밋은 진단을 한 문장으로 적어. "Every map app does this: a destination is a 2D point with no level." 지도한테 장소는 위도랑 경도야. 역은 여러 층에 문이 여러 개 달린 건물이고, 한가운데 점은 그 문 중 어느 것도 아니야. 제공자가 경로를 짜는 도보망엔 그런 건물 안 계단, 에스컬레이터, 층이 대부분 빠져 있어. GPS는 층과 층을 구별 못 하고. 그래서 턴바이턴 안내는 지도가 아는 데서 끝나. 역이라면 문보다 한참 앞에서.

더 나은 끝은 뭘로 만들어지나

coop 할 일 #369는 "level-aware arrival at large buildings (entrances, stairs, floors) instead of the map's center point"를 요구했어. 그게 아키텍처 불변식 21이 됐어. 큰 건물로 가는 도보는 그 건물 문에서, 그 문의 층에서 끝나. 부품은 이 트랙의 레슨 하나하나야.

  1. 중심 대신 문. Entrance는 점 하나, 걸어 도착하는 길에서 몇 층 위인지, 짧은 한국어 메모, 그리고 누가 그렇게 말했는지야. 제공자한테는 중심이 아니라 문으로 경로를 달라고 해.
  2. 증인 순위. 아빠가 직접 확인한 문이 피파 핀이 알려준 문보다 앞서고, 그 문이 OpenStreetMap 태그보다 앞서.
  3. OpenStreetMap한테 조심스럽게 묻기. Overpass 쿼리 하나로 역 건물 외곽선 위의 문이랑 길 건너 박물관 문을 갈라내.
  4. 기억. 폰은 알게 된 문을 간직해. 아빠가 확인한 문도, 아니라고 한 문도. 그래서 네트워크 없이도 역 입구를 다시 알아.
  5. 다른 방식의 마지막 100미터. 회전 안내는 끝. 대신 아빠가 보는 방향에서 문이 어디 있는지, 피파의 메모, 기압계가 내린 층 판정을 쉬운 말로 해. 문 바로 아래 길바닥은 도착이 아니야.

어떤 도보가 이걸 받나

카페 문은 핀에서 몇 미터 거리야. 그래서 도보 대부분은 예전처럼 끝나. 문이 필요할 만큼 큰 곳인지는 LargeVenue.matches가 정해. 이미 입구를 달고 온 장소면 통과. Apple이나 Google 분류가 기차역, 공항, 대중교통역이나 지하철역, 쇼핑몰, 경기장, 박물관, 대학이면 통과. 아빠가 다니는 곳들 언어로 그런 단어가 이름에 들어 있어도 통과야(station, estación, gare, Bahnhof, aeropuerto, terminal, mall, museo, catedral, 터미널, 공항, 백화점, 駅, 空港 등). 대소문자랑 악센트는 무시하고 비교해. 한국어엔 규칙이 하나 더 있어. 이름이 역이나 몰로 끝나면 돼. 서울역, 코엑스몰처럼. 끝날 때만이고 시작할 땐 아니야. 그래서 역삼동은 그냥 동네로 남아.

이제 곤란한 대목. "Madrid Puerta de Atocha"라는 이름엔 역이라는 말이 없어. 킷 테스트도 딱 그걸 확인해. 아토차 이름은 안 걸려. 피파 핀엔 분류도 없고. 살길은 이름이 아니라 지도 데이터에 있어. 목적지가 역 안에, 아니면 3층 이상 건물 안에 서 있다고 OpenStreetMap이 알려 주면 OSMEntrances.Reading.isLargeVenue가 참이 돼. 그럼 앱의 EntranceFinder가 앱이 도는 동안 그 중심을 적어둬. 이 모든 걸 시작한 역은 이름이 아니라 건물 덕에 자격을 얻는 거야.

설계를 붙잡는 경계가 둘 있어. 문으로 계획하는 건 도보뿐이야. 차로 아토차에 가면 경로는 건물로 가. 목적지가 이미 문으로 바뀌어 있었다면 운전 계획이 그걸 중심으로 되돌려. 보도 위 보행자 입구로 차를 보낼 순 없으니까. 그리고 시뮬레이션에선 아무것도 안 돌아. 시뮬레이션은 네트워크를 안 묻고 기압계도 없거든.

점에 닿았다고 장소에 닿은 건 아니야. 대상을 담는 모델이 대상보다 작으면(문 여럿에 층 여럿인 건물을 숫자 두 개로), 빠진 차원을 어딘가 적어둬야 해. 그리고 어떤 목적지에 그게 빠져 있는지도 시스템이 알아야 해.

Code

PathfinderKit, Arrival/Entrance.swift: LargeVenue.matches (원문 발췌)·swift
  public static func matches(_ place: Place) -> Bool {
    if place.entrance != nil { return true }
    let category = (place.category ?? "").lowercased().replacingOccurrences(of: " ", with: "")
    if categories.contains(where: { category.contains($0) }) { return true }
    let name = fold(place.name)
    if words.contains(where: { name.contains(fold($0)) }) { return true }
    let bare = name.trimmingCharacters(in: .whitespaces)
    return suffixes.contains { bare.hasSuffix($0) && bare.count > 1 }
  }
직접 쓴 최소 스케치: 단어 목록을 줄인 대형 장소 판정을 장소 여섯 곳에 돌려봄 (swiftc로 컴파일해서 실행)·swift
import Foundation

// A minimal sketch of Pathfinder's LargeVenue name test: a category, a word
// in several languages (case and accents folded), or a Korean suffix.
// Lists trimmed from the kit; the logic is the same.
let words = ["station", "estacion", "gare", "bahnhof", "airport", "aeropuerto",
             "terminal", "mall", "shopping", "museum", "cathedral",
             "터미널", "공항", "백화점", "쇼핑", "駅", "空港"]
let suffixes = ["역", "몰"]
let categories = ["trainstation", "train_station", "airport", "subway_station", "shoppingmall", "museum"]

func fold(_ text: String) -> String {
  text.folding(options: [.caseInsensitive, .diacriticInsensitive], locale: nil)
}

func isLargeVenue(name: String, category: String? = nil) -> Bool {
  let category = (category ?? "").lowercased().replacingOccurrences(of: " ", with: "")
  if categories.contains(where: { category.contains($0) }) { return true }
  let name = fold(name)
  if words.contains(where: { name.contains(fold($0)) }) { return true }
  let bare = name.trimmingCharacters(in: .whitespaces)
  return suffixes.contains { bare.hasSuffix($0) && bare.count > 1 }
}

for (name, category) in [
  ("Estación de Sevilla-Santa Justa", nil),
  ("서울역", nil),
  ("역삼동 카페", nil),
  ("코엑스몰", nil),
  ("Madrid Puerta de Atocha", nil),
  ("Madrid Puerta de Atocha", "Train Station"),
] as [(String, String?)] {
  print(isLargeVenue(name: name, category: category) ? "large " : "plain ", name, category ?? "")
}

External links

Exercise

실제로 다니는 큰 건물 세 곳을 골라봐. 역 하나, 공항이나 쇼핑몰 하나, 회사나 대학 건물 하나. 각각 지도 앱이 핀을 어디 꽂는지 찾고, 네가 진짜 드나드는 문을 찾아. 그다음 내비한테 건넬 입구를 적어봐. 문 좌표, 도착하는 길에서 몇 층 위인지, 처음 온 사람도 따라갈 수 있는 짧은 한 줄. 마지막으로 스케치의 이름 판정을 세 이름에 돌려봐. 어느 게 빠져? 그걸 잡아낼 다른 증거는 뭐야?
Hint
위성 보기랑 OpenStreetMap 지도 둘 다 건물 외곽선 위에 입구를 작은 표시로 보여줘. 층은 건물 1층 기준이 아니라 실제로 도착하는 길 기준으로 세. 같은 역 문이 한쪽 길에선 "두 층 위"고, 반대쪽 길에선 길이랑 같은 높이일 수 있어.

Progress

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

댓글 0

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

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