飛行機が大好きな幼児のために親が作ったレーダー
(jacobbartlett.substack.com)- 2歳の娘が飛行機に夢中になったことをきっかけに、周囲の航空機をスマホのレーダーのように表示するiOSアプリAviatorを数晩のサイドプロジェクトとして制作
- FlightRadar24は航空機の位置を地図上に表示してくれるが、実際の空でどの方向を見ればいいかや、子どもが見つけやすい飛行機がどれかは別途判断する必要があった
- アプリはCore Locationの方位情報、OpenSky Networkの非商用REST API、MapKitの注釈、SwiftUI UIを組み合わせ、航空機を端末の向きに合わせて表示する
- MVPテストで実際の飛行機とアプリ表示が一致することを確認した後、地図の非表示、低高度航空機の拡大、レーダーアニメーション、CRT効果、サウンド・ハプティクス、色設定を追加
- 最終的にアプリはApp Storeで公開され、その後の改善候補としてズームレベル、OpenSky高度API、出発・到着国表示、Metalシェーダー改善、距離・高度フィルター、zany modeが残っている
飛行機観察からアプリ要件へ
- 娘は夏の海外旅行で3時間のフライトを控え、飛行機での旅を楽しみにしており、搭乗後に客室乗務員の案内でコックピットを見学してからさらに飛行機に夢中になった
- その後は空の飛行機を探してほしいと頻繁に頼むようになり、庭で1時間のあいだ肩車したまま夕空の飛行機を探すことまであった
- FlightRadar24は地図上の航空機位置を表示してくれたが、実際の空でどの方向を見ればよいかはユーザー自身で合わせる必要があった
- 2次元地図では40,000フィートのLearjetとLondon City Airportを離陸したばかりのAirBusが似たように見えるが、実際の空では低高度の大型機のほうが見つけやすい
- 2歳児に必要だったのは地図の解釈ではなく、単純な飛行機探しだった
- 初期の問題は3つに整理できた
- 方向合わせ: 端末の向きに応じて航空機位置が実際の空の方向に対応する必要がある
- 大きさの表現: 航空機の高度に応じて画面上の大きさが変わる必要がある
- 使いやすさ: 業務用のフライト追跡アプリではなく、レトロな玩具のようなアプリであるべき
Aviatorの初期設計
- アプリのアイデアは「周囲のフライトをレーダーに表示する」で、プロジェクト名はAviatorに決めた
- 中核要件は3つ
- 端末が回転すると画面も一緒に回転し、航空機が正しい方向に表示される
- 航空機の高度に応じて画面上の大きさが変化する
- 子どもが使いやすい、楽しいレトロ玩具のような雰囲気を持たせる
- 方位処理にはiOSのCore Location APIのheading更新を使用
CLLocationManagerDelegateのdidUpdateHeadingで端末方位を受け取る- Combineの
CurrentValueSubjectでSwiftUIビューへ方位値を渡す
- 航空データにはOpenSky Networkを使用
- シンプルなREST APIで特定の緯度・経度範囲のリアルタイム航空データを提供
- 非商用利用は無料
- 現実的なレーダースイープのため、数秒ごとにendpointを呼び出す構想だった
- 位置データはCore Locationから取得し、ユーザー位置周辺の緯度・経度範囲をクエリする
- 初期構想では緯度±1度、位置精度0.1度で約10kmとなり、ユーザー位置を十分にぼかすつもりだった
- 実際のAPIサンプルコードでは、英国の緯度でおおむね正方形になるよう緯度0.5度、経度1度の範囲を使用
- OpenSky Network APIは非商用に制限されており、アプリも商用サービスではないため、SwiftUIではシンプルなMV構造を採用し、APIやLocationのような中核サービスだけを分離した
PoC: 方位、航空データ、地図検証
- 方位PoCでは
LocationManagerシングルトンが位置権限の要求、delegate設定、heading更新開始を担当 - SwiftUIビューは
.onReceiveでrotationAngleSubjectを購読し、四角形を円形に配置してコンパスのように回転させる効果を実装 - テスト端末では実際の方位変化にうまく反応したが、0度と360度を別値として扱うアニメーションのため、真北をまたぐと四角形が一周してしまう視覚的グリッチが発生した
- OpenSky REST APIは緯度・経度範囲を指定した
GETリクエストで地域のフライト配列を返す- サンプルendpointは
states/allにlamin、lamax、lomin、lomaxパラメータを付ける方式 - REST APIドキュメントはよく整備されていたが、レスポンスがキーなし構造のためJSONフィールドを順番にパースする必要があった
- Swiftの
UnkeyedContainerでicao24、callsign、origin_country、time_position、last_contact、longitude、latitudeなどをデコードした
- サンプルendpointは
- 航空機描画の最初の試みは、ユーザー位置と航空機の緯度・経度差に固定の画面スケールを掛けてSF Symbol
airplane画像を配置する方法だった- 緯度・経度1度の実距離は場所によって異なるため、正確ではありえない
- それでも出発点としては有用だった
- 精度検証のため、SwiftUIの
Mapを背景に置き、その上に航空機とコンパスを重ねた- FlightRadarの投影と比べると航空機数やクラスターはおおむね一致したが、位置はかなりずれていた
- その後、航空機を直接地図上の**注釈(annotation)**として描く方式へ切り替えた
MVP: MapKit注釈とユーザーテスト
- iOS 17をターゲットにしつつ、MapKitの地図注釈機能で飛行機型マーカーを実際の地理座標に表示した
FlightMapViewはMap(position:)上に航空機注釈を描画し、レーダー用途に合わせて.allowsHitTesting(false)で地図操作を無効化- 高度ベースのサイズ調整は、当初は高高度の航空機ほど大きく見えるよう対数スケールを適用していた
- 航空機の
true_trackとCore Locationのユーザー方位を組み合わせ、飛行機アイコンの向きを合わせた
- 航空機の
- 最初のMVPユーザーテストでは、実際に娘と飛行機を探し、アプリに表示された航空機が実際の空でも見えることを確認した
- テスト後、2つの修正点が明らかになった
- サイズ調整が逆だった: 空で目視しやすい航空機を示すには、低高度の航空機を大きくすべきだった
- 娘が欲しかったのは地図ではなく飛行機だけだったため、ノイズを減らすには地図の除去が必要だった
- 修正後のスケール式は
min(2, max(4.7 - log10(flight.geo_altitude + 1), 0.7))- ローカル上空のスキャンでは0.7から2.0の値となり、画面上の分布にうまく合っていた
レーダーUIを作る
- OpenSky APIはtimeout、
502 Bad Gateway、nullデータ入りの200レスポンスをしばしば返した- 無料の非商用APIでSLAもないため、クライアント側に基本的なリトライロジックを追加した
- 翌日には一日中うまく動作し、特定の高トラフィック時間帯を除けば概ね問題なさそうだった
- 実際の地図を見えなくするために
MapPolygonオーバーレイを使用- 本来は地図領域を強調するためのオーバーレイだが、航空機注釈だけを残して地図を覆う用途に活用した
- Appleのオーバーレイは地図の上、注釈の下に描画されるため、狙い通りの結果になった
- レーダー画面は線、同心円、20度幅の回転するangular gradientで実装
- その後、レーダーライン効果は360度のangular gradientに単純化した
- 緑から透明、黒へと続くgradientを回転させ、スイープと残像のように見せた
- 「grug-brained」なやり方のほうがうまくいくことが多いと評価している
- 端末を素早く回すと地図の断片が画面端に見える視覚的アーティファクトがあり、レーダービュー外を黒で覆うreverse maskで解決した
製品化: CRT効果、音、設定、App Store
- レトロ玩具らしさのためにCRT画面効果を追加
- iOS 17の
colorEffectに組み込まれたMetalシェーダー対応を利用 - シェーダーは黒以外のピクセルにscanline値を適用
timeパラメータを渡してscanlineが素早く上へ流れるようにし、動的な印象を与えた
- iOS 17の
- OpenSky Networkのポリシー上、App Store登録が問題ないか確認するため問い合わせたところ、20分以内に返信が来た
- レーダー体験とアクセシビリティ向上のため、フライト更新時にシステムサウンドとハプティクスを追加
AudioServicesPlaySystemSound(1052)でbeep-boop音を鳴らす- SwiftUIの
sensoryFeedback(.levelChange, trigger:)を使用
- 音が煩わしい場合に備え、
@AppStorageベースの設定を追加silent: サイレントモードshowMap: レーダーオーバーレイをオフにして下の地図を見られるuserColor: SwiftUI color pickerでレーダー色を選択
- Midjourneyの契約を止めていたため、Gencraftの非商用無料ジェネレーターを使って、航空帽をかぶった娘の雰囲気を持つAviatorマスコットを作成
- Apple Developer Programに再加入して£79を支払い、App Store配布を準備
- iOS 17のみを対象にしているが、6.5インチと5.5インチのiPhoneスクリーンショットを提出する必要があった
- 最新の5.5インチiPhoneであるiPhone 8 Plusは最大iOS 16までのため、AppScreensで両サイズのスクリーンショットを書き出した
- アプリはApp Storeで公開され、ダウンロードリンクはAviator — Radar on your Phone
次のバージョン候補
- 数晩で作ったサイドプロジェクトの結果に満足しており、娘のための玩具を作るコーディング体験は久しぶりに最も楽しかったという
- 次のリリース候補機能は以下の通り
- 地図ズームレベルを追加し、より近い航空機だけをレーダーに制限
- OpenSky Network高度APIでヘリコプター、衛星、飛行機サイズ等級を表示
- 航空機に出発・到着国表示のトグルを追加
- より高度なMetalシェーダーでCRT画面効果を改善
- コントロールを、サイズ変更可能なprogressive-disclosure pull-out modalへリファクタリング
- 距離・高度スライダーフィルターを実装し、低いが遠い航空機などを非表示にする
- UFO、巨大昆虫、エイリアンをレーダーに描画するzany modeを実装
1件のコメント
Hacker Newsのコメント
すばらしい作品だし、文章も良い。もともとのレーダー表示装置の設計が、このプロジェクトをどれほど強く方向づけているのかが興味深い
子どもは本物のCRTどころか、PPI表示付きのASR-9に触れる機会すらないかもしれないのに、それをシミュレートするためにかなりの努力を払っている
それはスキューモーフィズムが好きだからでもあるだろうが、元祖のレーダー画面が「空に何かあるか、そしてそれは自分から見てどこにあるか?」を見事に圧縮して示してくれるからでもあるように思える
最近の空観察用UIは、文脈情報や外部サービスへのリンク、広告スペースの方向へ進む中で、この問いに簡単に答える力を失ってしまった感じがある
目標物までの距離は中心からの距離で、方位は円周上の角度で表現されていた
ただ、スイープを真似た部分は再考の余地がある設計だったかもしれないし、明るい色と地図のない背景は画面の意図に集中させるので、まったく問題ないようにも見える
スマホを持って回るときにコンパス追跡機能がスイープの幻想を壊しているのも見えた
https://github.com/sky-map-team/stardroid
関連する技術要素はすべて理解していて、自分でもADSBステーションを運用しているが、空にカメラを向けると視界内の飛行機のメタデータが見えるのは今でも驚きだ
すばらしい。いつか作るものリストに入れておく
似た話だが、5歳の息子にフライトログブックを作ってあげて、赤ちゃんのころから始めた。飛行のたびに、客室乗務員に機長が書いてくれるか聞くようにしている
路線、機種、起きたことを記録していて、乗務員はこういうものが本当に好きだ。コックピットや乗務員休憩室も見せてもらったし、書いてくれるメッセージもいつも優しかった
返してもらうたびに子どもが満面の笑みになるので、小さな飛行機好きには強くおすすめする
初期リリースの必須機能として表示色の変更が入っているのが良い。対象ユーザー層のニーズをとてもよく理解している証拠だ
3歳児は色選択機能のあるおもちゃなら何でも好きだ
HNでkarmaが250を超えたときの不合理な興奮を想像すればいい
今日見せても、子どもが最初に気にするのは色を選ぶことだろうし、微細運動の発達にも良い
「飛行機の旅をあまりに持ち上げすぎたせいで、子どもが空港までタクシーで行く必要があることに驚いた。家からそのまま飛行機まで歩いて乗るものだと思っていた」というくだりが本当に愛らしくて面白く、しかもよく分かる
https://www.architecturendesign.net/john-travoltas-house-is-...
かわいい飛行機好きの子どもと一緒にいるのを客室乗務員が見るとコックピット見学をさせてくれる、という話では、NASAオレンジのジャンプスーツをおそろいで着るのはあらゆるコックピットに入るためのパスポートだ
子どもたちより乗務員のほうがさらに盛り上がるし、空港で子どもを見つけるのもとても簡単になる
失敗したソーシャルネットワークアプリTapstackを家族用だけに複製した記事 An app can be a home cooked meal も関連している
https://www.robinsloan.com/notes/home-cooked-app/
もちろんバズるのも悪くはない
細かな気になる点が二つある
飛行機の点が描かれたあとに動くと、少し没入感が削がれる。スイープによって点が「塗られ」、次のスイープまでは固定されていたほうが、より現実的に感じられそうだ
さらにリアルさを出すなら、前のデータポイントから外挿して、各飛行機がスイープごとに一貫して移動するようにすることもできる
二つ目はむしろもっと単純かもしれない。APIが飛行速度を返すので、データポイント一つだけでも計算できる
娘と一緒にできるアクティビティとしては、最寄りの国際空港の滑走路着陸側の端にある道路、駐車場、公園に行くのがおすすめ。
たいてい風向きによって変わるので、それに合わせて行き、車を止めて飛行機が進入して着陸するのを見ればよい。
子どもは自分のレーダーでどの飛行機が来るか見られるし、うちは flightradar24 を使った。
進入中の大型ジェット機が地面のすぐ近くまで降りてくる様子に子どもたちはすっかり夢中になり、何時間でも簡単に楽しめた。次の飛行機はもっと大きいか、もっと近いかを見ると言って帰りたがらず、交渉が必要だった
Airbus A318 サイズ以下の多発固定翼機の中でも、5.5°進入を実施できるよう特別な航空機・乗員認証を受けたものだけが運航可能。
https://www.youtube.com/watch?v=yZIDFgpT0-o
https://en.wikipedia.org/wiki/London_City_Airport
このアプリにはひとつ問題がある。アプリページにははっきり 4歳以上 と書かれているのに、想定ユーザーは2歳だ。
よくできている
素晴らしい。
細かい質問だけど、CRT レーダースコープには実際に走査線があったのだろうか? ベクターディスプレイだと思っていた。
追加機能の話でいうと、飛行機観察用アプリなのだから、点をタップすると数秒間機種を表示してくれると素敵だと思う
蛍光体の残光のおかげで、強いレーダー反射は画面にしばらく残った。
その時間内にレーダーが一回転以上すれば、同じ飛行機が少し動いた新しい点、つまり「plot」として見えた。
フェルトペンで画面上の plot をなぞって「track」にすることもできた。
レーダー画面に向けて映画用カメラを置き、数時間分のレーダー反射を録画して後で再生する特殊な画面もあった。
たとえば 1968 年のチェコスロバキア革命時におけるワルシャワ条約機構航空機の加速再生記録がある: https://youtu.be/rAUodXI4LPw?t=622
もともとのレーダー画面は、中心から外側へ放射状にスキャンしていた。スキャンのタイミングは距離がスケーリングされるようあらかじめ決められており、ビーム強度信号は増幅されたレーダー反射信号そのものだった。
そのため、より強い反射信号は長残光スクリーン上でより見えやすい「blip」として現れた。
面白いのは、レーダービームが CRT 画面の周囲をスキャンするようにするため、陰極線管の電子銃アセンブリ全体を回転するレーダー皿と同期したモーターで駆動していた点。
この回転は常にレーダー皿の速度と方向に一致していなければならず、そうでないと blip はおかしな位置に現れた。
固定された方位線と距離線は、CRT 管自体か透明カバーに印刷されていた。こうした画面は何十年も使われ、おそらく 1980 年代か 1990 年代初頭までは残っていた。
もっと新しい版では、複雑な回転式ビーム放出アセンブリを避けるため、単純な電子回路で X/Y 方向を独立にスキャンできた。
詳細: https://en.wikipedia.org/wiki/Radar_display#Plan_position_in...
角度は偏向板を物理的に回転させるか、sin/cos を電子的に計算して X/Y 偏向板に適用することで作れる。
返り信号の振幅がビーム電流を直接駆動するため、反射の大きい目標ほど明るく見える。
これに 蓄積管効果 も加わるが、初期コンピュータの蓄積管 X-Y ベクターディスプレイにあった双安定モードではなく、一部オシロスコープの可変残光モードのように動作する。
この特性のため、SR-71 や XB-70 のような初期ステルス機設計の試みと関係する結果も生じた。
レーダー管では複数回のスイープにわたって「blip」が蓄積されるようビーム電流が設定されており、通常 blip はスイープ間で自分の直径より小さい距離しか動かない。
しかし、航空機のレーダー反射断面積が本質的に低く、しかも速すぎて blip がスイープ間でより遠くへ移動すると、特に目標が多くて慌ただしい状況では、レーダー操作員が小さなゴースト blip をノイズだと解釈してしまうことがある