Share
Embed
幾何アルゴリズムで なめらかなピン操作を / iOSDC Japan 2026 / smoothpin
2026/09/12 15:35〜Track Bレギュラートーク(20分)
https://fortee.jp/iosdc-japan-2026/proposal/3c119e7a-235a-4073-b00f-6024063e229c
nade
September 12, 2026
Transcript
55 slides
-
移動で地域をカラフルに newmo 幾何アルゴリズムで なめらかなピン操作を iOSDC Japan 2026 Track B /
レギュラートーク(20分) 永野 一馬 / なで @nade_kaz -
自己紹介 永野 一馬 / なで(@nade_kaz) 2025年3月〜 newmo。タクシー配車アプリと、点呼・給与・勤怠などタク シー会社のシステム DX iOS
だけでなく Web・Backend も / なで肩 ⇨ なで / 数学は専門外 新大阪駅で実証実験中(〜10月):事前予約で newmo 専用乗り場から並ば ずに乗れます。大阪にお越しの際はぜひ タクシー配車アプリ「newmo」 2024年12月 大阪から提供開始 自動点呼「newmo点呼」 国交省認定 2025/7・2026/2 新大阪駅 予約タクシー実証実験 JR西日本と 2026/8〜10。自動運転との 結節を見据えて 2 -
今日の話の元ネタ tech.newmo.me ・ 2025-12-15 Coding Agentと幾何学アルゴ リズムを活用した地図上の滑らか なピン操作の実現 tech.newmo.me ・
2026-02-24 【お蔵入り】ライドシェアド ライバーアプリのSwiftUI作 品集! tech.newmo.me ・ 2025-12-11 プロダクトチーム全員でAIエ ージェント縛りの開発を一週 間試しました 今日の話の元ネタ 3 -
今日のテーマ 数学ガチ勢の 講義? 幾何アルゴリズム 🤔 地図 SDK の 使い方講座? AI
に全部 書かせた話? 4 -
今日のテーマ 幾何アルゴリズム 🤔 ❌ ❌ 数学ガチ勢の 地図 SDK の 講義?
使い方講座? ❌ AI に全部 書かせた話? ⇨ 地図 UX を、幾何(= 座標変換や図形計算)で 「なめらか」にする話 5 -
なぜ、いま幾何なのか? 1 幾何は 20 世紀に枯れた知識 凸包も Graham Scan も 1972
年。実装例が豊富で、Agent の精度が高い 2 純粋なロジックは TDD と相性がいい 成功・失敗が明確。期待値をテストで書き、実装は Agent に任せる 3 Agent から数学を学ぶのは楽しい 「なぜこの式?」をその場で聞ける。手計算より Agent の方が正しかったことも 6 -
今回のゴール 座標変換と図形計算を、 自分のアプリの UX 改善に使える 現在地ガイドライン、ピンの押し出しといった マップ上のリッチな機能に、幾何がどう活用されているか 提供された API に必要なアルゴリズムがなかった場合に
「自分で作る」という選択肢を選べるように 学生時代ぶりの知識に社会人になってから出会ったときの、 ちょっとした喜び 7 -
1 現在地とピンを結ぶ ガイドライン 見たことのある、あの点線。その描画と軽量化 Smooth out the movement, back and
forth, over there and here. newmo -
乗車位置をピンで決める 乗車位置 現在地 1 地図を動かして、中央のピンで乗車 位置を決める 2 「ここで乗る」で確定 3 配車
⇨ ドライバーがその場所へ ここで乗る 11 -
実現①:現実世界の座標で、地図上に結ぶ 描画済みの領域(タイル) 画面 画面外にも描いている 現在地 現在地とピン(緯度経度)を、地図側 の Polyline で結ぶ 現実世界
⇨ 地図 素直。でも重い 地図は、画面より大きな巨大なスクロ ールビュー。見えない所まで描く そこに、指を動かすたびに変わる線を 描き直す ⇨ 全体の更新に巻き込まれ る 14 -
そもそも「座標」って、どこにあるんだっけ? 現実世界 緯度・経度 実際の場所 ⇄ 地図 位置・ポリゴン タイルの上 ⇄ スマホ画面
ピクセル(CGPoint) 見えている範囲だけ 現実世界 ⇨ 地図 をやめて、現実世界 ⇨ 画面 で解く 15 -
実現②:現実世界 ⇨ 画面の座標へ(軽量化) 現在地(緯度経度)を、地図の投影で画面上の CGPoint に変換 地図の上に別レイヤーを重ね、SwiftUI の Path で点線を描く
地図ビューには何も渡さない ⇨ 地図は再描画されない 描くのは画面の中だけ。現在地が画面外なら、画面枠との交点まで 16 -
参考:【数学B】直線のベクトル方程式 媒介変数 t P(t) = C + t × (T
− C) 画面(x = xmax の辺) ピン(画面の中心) T 現在地 t C から T に向かって「何割」進んだか(0 〜 1) P(t) その割合の位置にある点 C C(t = 0) t = 0.5 枠に当たる t T(t = 1) つまり 上下左右の辺ごとに「その辺に着く t」 を計算し、いちばん小さい t(最初にぶつかる辺) で線を止める 18 -
画面枠との交点 — コード — // 画面枠との交点比率 t を x, y
それぞれで求め、小さい方を採用 let tx = dx == 0 ? .infinity : ((dx > 0 ? bounds.maxX : bounds.minX) - center.x) / dx let ty = dy == 0 ? .infinity : ((dy > 0 ? bounds.maxY : bounds.minY) - center.y) / dy let t = min(tx, ty) return CGPoint(x: center.x + dx * t, y: center.y + dy * t) 19 -
参考:【中3】三平方の定理と単位ベクトル ベクトルの大きさ・単位ベクトル C(ピン) ここから描く(r + 線幅) |v| = √(vx²+vy²) e(長さ1)
T(現在地) vx |v| = √(vx² + vy²), e = v ÷ |v| 現在地からピンへ向かう矢印(横 vx、縦 vy) |v| その矢印の長さ(三平方の定理) e 長さを 1 にした矢印。向きだけを表す r 現在地マーカーの半径 v vy つまり 始点 = 現在地 + e × (r + 線幅)。「ピ ンの方向に、r + 線幅 だけ進んだ点」から線を 描く 21 -
マーカーを避ける — コード — // 現在地 → ピン方向の単位ベクトル let ux
= v.x / length, uy = v.y / length // マーカー半径+線幅ぶん、ピン側へ寄せた点を始点に let adjusted = Self.currentLocationPinRadius + Self.lineWidth return CGPoint(x: target.x - ux * adjusted, y: target.y - uy * adjusted) 22 -
2 乗車禁止エリアと ピンの押し出し 地図上のポリゴンと、描けないエリア Smooth out the movement, back and
forth, over there and here. newmo -
じゃあ、自動で外に押し出したい 置けない場所に来たら、 置ける場所へそっと逃がす どこへ? ⇨ いちばん近い、エリアの外 いつ? ⇨ 指を離した時 どうやって?
⇨ 地図上の幾何で解く 非推奨エリ 置ける場所に収まった 指を離すと、 いちばん近い外へ地図がすっと動く 29 -
いつ、何をする? 1 地図を動かしている間:内外判定だけ ⇨ ピンの色と吹き出しを切り替える 2 指を離した瞬間(カメラ停止):エリアの中なら、外の座標を計算 3 その座標へカメラをアニメーションで移動 ⇨
ピンが「そっと逃げる」 ドラッグ中は判定だけ。重い計算は止まった時に1回(ここも一度、性能で直した) 30 -
いまここ 現実世界 緯度・経度 ⇄ 地図 位置・ポリゴン 幾何が住む場所 ⇄ スマホ画面 ピクセル(CGPoint)
ポリゴンとピンの位置関係 ⇨ 地図上の幾何として解く 31 -
押し出しは3ステップ 1 各辺への最近点(垂線の足)⇨ いち ばん近い辺を選ぶ 2 押し出す向き:ピン ⇨ 足 の直線
(辺に垂直で、外向き) 3 その向きに一定距離ずらす 禁止エリア ピン 垂線の足 32 -
参考:【数学B】内積と正射影 正射影ベクトル (P − A) · (B − A) t=
, |B − A|² P(ピン) AH = t · AB A(t = 0) B(t = 1) H(垂線の足) H = A + t × (B − A) 辺の両端の点 P ピン H P から辺に下ろした垂線の足。辺の上で P にいちばん近 い点 t A から H までが、AB の「何割」か · 内積。x 同士と y 同士を掛けて足す A, B つまり t が 0〜1 なら H は辺の上。0 より小さ ければ A、1 より大きければ B がいちばん近い ⇨ max(0, min(1, t)) 33 -
最近点は t を 0…1 に丸めるだけ // 線分 start→end 上への射影パラメータ t
を 0...1 に clamp let t = max(0, min(1, (px * dx + py * dy) / lengthSquared)) // 線分上の最近点(垂線の足)。t が 0 か 1 なら端点 let foot = CLLocationCoordinate2D( latitude: start.latitude + t * dy, longitude: start.longitude + t * dx ) 辺ごとに計算 ⇨ 距離が最小の辺を採用 34 -
押し出す向き 通常 ⇨ ピン → 足 の直線 境界線上 ⇨ 辺の法線
辺に垂直で、外向き 直線の長さ 0 ⇨ 向きが決まらない ピン(辺の上) ピン(エリア内) 2方向ある ⇨ 外側の方 境界線上(共有辺・隙間ゼロ・再帰の途中)では直線の長さが 0 ⇨ 辺に垂直な向きのうち、外側になる方を選ぶ 35 -
参考:【中学】垂直な直線と法線ベクトル 法線ベクトル に垂直な矢印は (−d , d ) または (d ,
−d ) d = (dx, dy) ( dy , − dx ) 2方向ある ⇨ 外側の方を採用 y x y x 辺に沿った矢印(横 dx、縦 dy) 法線 辺に垂直な矢印。x と y を入れ替えて片方の符号を反 転すると 90° 回る ± 垂直な矢印は表と裏の 2 本ある d d = ( dx , dy ) (−dy, dx) つまり 2 本のうち、少し進んでみてポリゴンの 外に出る方を採用。コードでは辺の方位角 ± 90° で同じことをしている 36 -
押し出す向き — コード — // 境界のすぐ近くでは法線、離れていれば直線 if distanceBetween(coordinate, foot) <
1.0 { calculateNormalHeading(at: foot, on: path) ?? 0 } else { computeHeading(from: coordinate, to: foot) } 境界線上の判定は「足まで 1m 未満」。法線は2方向あるので、少し進んで外側になる方を採用 37 -
どれくらい逃がす? — メートルを緯度経度に戻す — 「2m 逃がす」の 2m は距離。緯度経度は角度 足の位置と向きは、緯度経度を平面とみなして計算できる (数十
m の範囲なら誤差は無視できる) でも「その向きに 2m」は、緯度経度にそのまま足せない 方位角と距離から、球面上で 2m 先の緯度経度を求めてカメラに渡す 最後は必ず緯度経度に戻す。地図を動かす指示は、緯度経度でしか出せない 38 -
参考:【数学II】弧度法と弧の長さ l = Rθ l=R×θ θR l = Rθ 緯度・経度は「角度」
地球:R ≈ 6,371 km ⇔ θ=l÷R 地球の半径。約 6,371 km θ 中心角。ラジアン(360° = 2π) l 弧の長さ = 地面の上を進む距離 R つまり 緯度経度は「角度」。2 m 進みたいなら θ = 2 ÷ 6,371,000 ラジアンだけ角度を動か す。コードの distance / R がこれ 39 -
これで完成、、? 隣接エリアは、想定より難しかった 😇 A から押し出した先が、隣の B B の判定が働き、A へ押し戻す 境界で振動し、無限ループに
外部データの隙間・重なりを手で直 す? ⇨ 無理 A B A⇨B⇨A⇨B⇨… 40 -
3 隣接エリアと 凸包による統合 逃げた先にも、エリアがあったら? Smooth out the movement, back and
forth, over there and here. newmo -
発想:回避領域を動的に広げる 「押し出し先がまた禁止エリアなら、そこも取り込む」 1 2 3 4 ピンが A ⇨ A
の外へ押し出す 押し出し先が B ⇨ A+B を統合 統合エリアの境界から、もう一度 入らなくなるまで再帰(上限5回) 外殻の外へ、一発で脱出 A+B を凸包で1つに 凸包(Convex Hull)= 点の集まりを輪ゴム で囲んだ外形 42 -
そもそも凸包アルゴリズムの歴史 凸包の歴史① 1972 Graham Scan 極角でソート ⇨ 左折だけ残す。O(N log N)
凸包の歴史② 1973 ギフト包装法(Gift wrapping) 包み紙を巻くように外周をたどる。簡単、O(N·h) 凸包の歴史③ 1979 Monotone chain(Andrew) x でソート ⇨ 上下の殻を作る。Graham の派生 参照:Wikipedia「Graham scan」「Gift wrapping algorithm」/ 50年前に枯れている(前提 1) 43 -
検討:統合の仕方 1 ポリゴン結合(Polygon Union)— 断念 凹みは保てる。でも低速、自己交差で壊れやすい 2 凹包(Concave Hull)— 断念
大小さまざまなエリアでは壊れやすい 3 凸包(Convex Hull)— 採用 O(N log N)、枯れている。凹みは消える 4 外接矩形(Bounding Box)— 断念 最速だが L 字を潰しすぎ 44 -
なぜ凸包? 完璧な形状 < 速さと堅牢さ 指を離すたびに走り、カメラが動く前に終わる ⇨ 速さ 外部データの隙間・重なりに耐える ⇨ 堅牢さ
1972年から枯れている ⇨ 実装が安定(前提 1) 45 -
注意点:凸包は凹みを埋める 凸包は、凹みを埋めてしまう 膨らみ(Bulge) 本当は置けるのに避ける A 点線 = A+B の凸包 B
影響が出るのは「押し出しでループし た時」だけ 直接そこに置けば、制御は走らない 「自動で逃がす時だけ、少し広めに避 ける」割り切り 46 -
コード(骨格・簡略化) // 通過した禁止エリアを凸包で1つの境界に let unified = createUnifiedPath(from: accumulated) let foot
= findClosestEdge(to: origin, on: unified).foot let next = computeOffset(from: foot, distance: 2, heading: h) // 押し出し先が、まだ別の禁止エリアの中? 安全なら終了 let hits = allRestricted.filter { $0.contains(next) } if hits.isEmpty { return next } // 見つかったエリアを蓄積して再帰(起点は変えない・上限5回) return push(origin, accumulated: accumulated + hits, depth: depth + 1) 47 -
参考:【数学B】外積の符号と回転の向き 2次元の外積(行列式) c b 右折 ⇨ 負 ⇨ 捨てる a
左折 ⇨ 正 ⇨ 残す c b a (bx − ax)(cy − ay) − (by − ay)(cx − ax) 順番に並んだ 3 つの点 外積 a→b と a→c の 2 本の矢印が「どちら回りか」を表 す数。三角形の面積公式 ½|x1y2 − x2y1| の中身 a, b, c つまり 正なら反時計回り(左折)で残す、負な ら時計回り(右折)で捨てる、0 なら一直線。極 角は atan2 で求めてソート 49 -
Graham Scan — コード — // 起点を基準に極角でソート(O(N log N) の肝)
let sorted = points .filter { distanceBetween($0, start) >= 0.1 } .sorted { polarAngle($0) < polarAngle($1) } // atan2 // 左折だけ残す(O(N))。外積の符号で右折を落とす return sorted.reduce(into: [start]) { hull, p in removeRightTurns(&hull, newPoint: p) hull.append(p) } 50 -
Before / After — 隣接エリアの押し出し — Before:今いるエリアだけ見る A B After:入った先を取り込む(凸包)
A B C C 6 回目:A の外へ ⇨ B の中 …終わらない 3 回目:起点から A+B+C の凸包の外へ ⇨ 脱出 同じ位置から開始。Before は A と B を往復し続ける After は起点を固定したまま凸包を広げ、3 回目で外へ 51 -
体験の変化 — 従来 vs 現在 — 課題 現在地を見失う 点線の描画 非推奨エリア
隣接エリア 従来 ピンだけ動く 地図側で描いて重い・消える 押してからエラー、どこがセーフか不明 往復して止まらない 現在 点線で結ぶ 画面座標で、画面内だけ描く 置ける場所へ、そっと押し出す 凸包で統合、一発で脱出 53 -
まとめ 1 現在地との関係は線で見せ、置けない場所は操作で伝える 2 座標変換は「どの座標系で解くか」を選ぶ道具 点線は画面座標で描く、押し出しは地図上の幾何で解く 3 隣接エリアは「回避領域を動的に広げる」。完璧な形状 < 速さと堅牢さ
4 枯れた数学は Agent が得意。テストで期待値を書く 54