「暗い時間」が見える旅行アプリを作っています — Daylight Trip 開発記

冬のヨーロッパを旅行したとき、まだ真っ暗な早朝にバス停へ向かう時間が、正直言って怖かった。

日の出は朝8時40分。ホテルを出るのは7時半。地図アプリは道順を教えてくれるけれど、「その道が暗いかどうか」は教えてくれない。事前に分かっていれば、送迎を頼む・予定を1時間ずらす、という選択肢があったはずなのに。

既存の旅程アプリはどれも高機能で、予定を「詰め込む」方向にばかり進化しています。そうではなくて、明るい時間・暗い時間を見える化して、安全で快適な旅程を組むためのアプリが欲しい。なければ作ろう、ということで、いま Flutter で開発中なのが「Daylight Trip」です。

Daylight Tripの画面構成(タイムラインと俯瞰ページ)
Daylight Tripの画面構成(タイムラインと俯瞰ページ)
目次

このアプリでできること

画面は2つだけです。

タイムラインページは、1日を縦スクロールで表示します。左端の細い縦のバーが「時刻の定規」になっていて、1日の光の移り変わりを4色のグラデーションで塗り分けています。

濃紺 → 薄いオレンジ → 白 → オレンジ → 濃紺
夜     朝の薄明      昼    夕方の薄明   夜

この4色は飾りではありません。外側の2本の境界は市民薄明(civil twilight)——「照明なしで屋外の活動ができる明るさ」の境界です。つまり薄いオレンジの帯は「歩いても大丈夫か」の判断基準そのもの。意味があるから4色なんです。

予定はカテゴリごとに色分けされ(観光・食事・ツアー・ホテル)、日の出・日没には時刻入りの区切り線が入ります。「17:30のホテルチェックインは日没の12分後、つまり暗くなり始めた頃」といったことが、文字を読まなくても分かります。

俯瞰ページは、1行=1日の24時間バーを縦に並べたもので、このアプリの主張が最も強く出る画面です。東京→パリ→オスロ→トロムソと北上する7日間のデモ旅程では、日中の白い帯が行を追うごとに目に見えて痩せていきます。最終日のトロムソでは、白がほんの細い隙間しか残りません。冬の高緯度の怖さが、説明なしで伝わる。バーの下には各予定が「●━━●」の区間線で重なるので、暗い時間の上に乗っている予定——真っ暗な朝の移動、夜のオーロラツアー——が一覧で炙り出せます。旅程表というより、安全点検表に近い感覚です。

俯瞰で気になる日を見つけたら、日付をタップするとその日のタイムラインへジャンプします。

工夫した点

時刻軸は等間隔、ただし深夜だけ折りたたむ。

予定がある時刻だけを行にすると、2時間の空白と30分の空白が同じ高さになり、グラデーションが「時間の長さ」について嘘をつきます。夜の長さを見せたいアプリなので、これは致命的。だから1時間=固定高さを貫き、空白が長くなる深夜帯(0:00〜5:00)だけ、予定がない日に限りワンタップで折りたためるようにしました。深夜にフライトがある日は自動的に折りたたみ不可になるので、予定が隠れる事故は起きません。

移動ブロックは「両端だけ正確」に。

成田を朝に発ってヨーロッパに夕方着く便で、飛行中の背景を出発地と到着地どちらの日照で塗るべきか——実は正解がありません(機体が動き続けるので)。そこで中身を正しく塗ることは諦めて、全面を灰色の斜線ハッチングにし、代わりにブロックの両端に「出発 08:05 東京」「到着 15:45 パリ」のチップを置きました。本当に知りたいのは「空港へ向かうとき暗いか」「降りた瞬間に暗いか」で、それはまさにこの両端の情報。機内が明るいかどうかは重要ではないんです。

ベタ塗りではなく斜線ハッチングなのも理由があります。

このアプリは「印刷して持ち歩ける」ことを前提にしていて、モノクロ印刷すると灰色のベタは濃紺と潰れて区別がつきません。斜線なら形で判別できるし、色覚特性への配慮にもなります。

完全オフラインで動きます。

日照計算は天文計算をローカルで行い、都市データ(約3.4万都市)もアプリに同梱。旅先の機内モードでも、電波のない山の中でも動きます。

開発者向け:技術的なポイント

ここからは Flutter 開発者向けの話です。

日照計算は「UTCで計算、表示直前に変換」

計算には daylight パッケージ(天文計算・API通信不要)と timezone パッケージを使っています。

final calc = DaylightCalculator(DaylightLocation(lat, lng));
final official = calc.calculateForDay(date, Zenith.official); // 日の出・日没
final civil    = calc.calculateForDay(date, Zenith.civil);    // 市民薄明

ハマりどころが2つありました。ひとつは toLocal()。公式サンプルにある整形関数は内部で toLocal() を使っていて、端末の現在地(日本)の時刻に変換されてしまいます。旅行アプリでこれは致命傷なので、必ず tz.TZDateTime.from(utc, location) で「その都市のタイムゾーン」へ明示的に変換します。計算はUTC、変換は表示直前——この順序を守れば、時差もサマータイムも自動的に正しくなります(パリの2026年3月の DST 移行日で、日の出が06:39→07:37へ1時間ジャンプするのをテストで固定しています)。

もうひとつは tzdata のリンク名。timezone/data/latest.dart は Europe/Oslo のようなリンク名を解決できず例外を投げます。GeoNames の都市データにはリンク名がそのまま入っているので、対策しないとオスロやコペンハーゲンで落ちます。latest_all.dart に切り替えて解決しました。

極夜・白夜は null と DayType で分岐

高緯度では sunrise が null になります。sunrise! と書いた瞬間、このアプリを最も使いたい「冬の高緯度」でクラッシュするわけです。実測で確認した挙動はこうでした。

  • 極夜(トロムソ 12/21): DayType.allNight。日の出・日没は null だが市民薄明だけは返る(09:30〜13:52)。バーは白のない「紺→薄橙→紺」の3色になる
  • 白夜(トロムソ 6/21): DayType.allDay。4境界すべて null。バーは全面白

グラデーション生成を純関数にしておいたので、この3パターンはすべてユニットテストで固定できています。

時差をまたぐ移動ブロックの配置

タイムラインの時刻軸は「その日を過ごす都市」のタイムゾーンで引かれています。でも移動の出発点は別の都市の現地時刻。そこで、端点をいったん UTC の瞬間に直してから軸のタイムゾーンへ換算する関数を1つ用意しました。

// 東京 08:05 JST は、パリの軸では 00:05 に置かれる
final f = fractionOnDay(
  point: 出発点(都市・日付・現地時刻),
  axisTimeZone: 'Europe/Paris',
  axisDate: その日,
); // → 0.0〜1.0(前日なら負、翌日なら1.0超)

これで「東京を朝8時に発つと、パリの1日はほぼ丸ごと移動で消える」という長距離フライトの実感が、そのまま画面に出るようになりました。

深夜の折りたたみとグラデーションの追従

深夜帯の折りたたみは「時刻→ピクセル」の変換を1クラス(FoldedTimeAxis)に集約して実現しています。
面白いのは日照バーのグラデーションで、LinearGradient の stops も同じ変換を通すことで、紺の帯だけがクシャッと圧縮される動きになります。stops が単調増加でありさえすれば LinearGradient は正しく描いてくれるので、変換の単調性だけテストで保証しています。

都市データは GeoNames を人口1.5万人以上に絞って同梱

オフライン動作のため、GeoNames 由来の都市データ(名前・日本語名・緯度経度・IANAタイムゾーン識別子)を JSON でバンドルしました。人口15,000人以上に絞ると約3.4万都市・2.2MB。日本語名は別名リストから「かなを含む最短のもの」を採用するヒューリスティックで約9,100都市をカバーし、「パリ」でも「tokyo」でも検索できます。

大事なのは、タイムゾーンをUTCオフセットの数値ではなく IANA 識別子で持つこと。数値で持つと、3月末・10月末の DST 切り替えをまたいだ瞬間に1時間ずれます。そして旅程ファイル自身にも緯度経度とタイムゾーンをコピーして保存するので、都市データベースのバージョンが変わっても、過去の旅程は同じ計算結果を返し続けます。旅程は1旅行=1つの整形済み JSON ファイル。ユーザーのデータを人質にしない、という方針です。

これから

旅程の編集UI(都市検索から予定を入れる流れ)と、印刷して持ち歩くためのPDF出力を実装したら、リリースに向かう予定です。機能は増やしすぎない——このコンセプトは、絞るほど魅力が出ると思っているので。

進捗はまたブログで報告します。冬の暗い朝にバス停で不安になったことのある方、リリースをお楽しみに。


Daylight Trip は SINQWELL が開発中の Flutter アプリです(Android / iOS 予定)。

目次