能登の祭りを盛り上げろ!巨大曳山行事"でか山"を追いかけるアプリを開発した
毎年5月2日〜5月5日、ゴールデンウィークに石川県七尾市では「青柏祭」という巨大なお祭りが開催されます。昨年は令和6年能登半島地震の影響で中止になり、今年は2年ぶりの開催で大盛況が予測されています。
そんな青柏祭の巨大曳山行事「でか山」をリアルタイムで追跡するWebアプリ"でか山ウォーク"を開発しましたので、技術選定や開発現場のことについて綴ります。
開発背景
巨大曳山行事「でか山」は、総重量約10トンにも及ぶ山車を曳き手が力を合わせて動かす壮観なイベントです。しかし、3つの曳山が街中を練り歩くため、どの山車がどこを通っているのか、見物客には追いかけ切れないという課題がありました。
そこで今年は、「でか山ウォーク」として、曳山の位置をリアルタイムで可視化するWebアプリを開発。会場にいながら、スマホやブラウザで現在位置を把握できるようにし、より快適に祭りを楽しめる体験を提供する運びとなりました。
必須要件
-
でか山の位置取得
- 曳山に設置したスマホ端末から位置情報を送信
- 可能な限りリアルタイム性を担保(更新間隔1〜5秒程度)
-
ユーザー向けマップ表示
- 地図上に3つの曳山アイコンを設置(各町の紋章をアイコンに)
- でか山の位置に飛ぶボタンを設置し、すぐに居場所が分かるように
- 周辺のトイレ・駐車場の情報も掲載、
- 周辺施設の空き状況をユーザーが1タップで投稿できるように
- 現在地を表示
-
スムーズな操作性
- WEBアプリだが、なるべくネイティブライクな UI / UX
- 低スペック端末でも動作する軽量設計
-
不規則なトラフィックに対応する設計
- 例年の来場予測は7万人。今年は震災後初という事で、さらに増える予想。初回でアプリ利用数も予測が不能なため、サーバーレスアーキテクチャを採用し、スケーラビリティを重視。
- GA4とFirebaseのUsageで4日間のアクセス数を集計し、来年以降のシステム構成と通信費管理の計画に活かす。
システムアーキテクチャ
-
インフラ
- Firebase Realtime database
- 位置情報テーブルの 緯度 と 経度 を都度更新
- Vercel
- Reactアプリのデプロイに使用・
- Google Cloud Monitoring
- エラー検知、コストオーバー時のSlack通知など
- Firebase Realtime database
-
バックエンド
- GPS実機
- スマートフォンレンタル6台(Redme12 5G / Xiaomi)
- Kotlinで位置情報送信するだけのアプリを開発。
- Firebase App Distribution で6台の実機に配布(3つのでか山に各1機、予備1機)
- GPS実機
-
フロントエンド
- Vite × React
- Gppgle Maps API
今回の実装における要点
◼︎スマートフォンをGPS機に変身させるアプリ
今回、スマートフォンのレンタルにはジャパン・エモーション様の法人スマホレンタルサービスを利用しました。迅速に、丁寧に対応してくださいました。
そしてスマートフォンをGPS機として使うために、Androidの機能にある Foreground Service を採用しました。
フォアグラウンド サービスを使用しているアプリの例を次に示します。
この音楽プレーヤー アプリは、フォアグラウンド サービスで音楽を再生します。通知として、いま再生中の曲が表示されます。
このフィットネス アプリは、ユーザーからの許可を得たうえで、ユーザーのランニングをフォアグラウンド サービスで記録します。通知には、現在のフィットネス セッション中にユーザーが移動した距離が表示されます。
Androidアプリを採用した理由としては、ロック時・スリープ時・アプリをスワイプ終了していても、明示的にアプリ内で stop ボタンを押すまで途切れることなく位置情報を firebase に送信し続けることができるためです。
iPhoneで実装したことは無いですが、参考記事やAIにも聞いて、スリープ時やアプリ終了時の挙動がやや異なり、Android の方が適切という判断をしました。
あと、短期スマホレンタルの料金が安いのも嬉しいポイント。
◼︎位置情報の送信を開始・終了するコード
3つのでか山のうち、どれに搭載するかを選んで「位置情報の共有を開始」するだけで完了です。
private fun startLocationService(deviceId: String) {
Log.d(TAG, "startLocationService: Starting service with device ID: $deviceId")
val serviceIntent = Intent(this, LocationForegroundService::class.java).apply {
putExtra("DEVICE_ID", deviceId)
}
startForegroundService(serviceIntent)
statusText.text = "位置情報サービスを開始しました($deviceId)"
}
private fun stopLocationService() {
Log.d(TAG, "stopLocationService: Stopping service")
val serviceIntent = Intent(this, LocationForegroundService::class.java)
stopService(serviceIntent)
statusText.text = "位置情報サービスを停止しました"
}
殆どこれが全ての簡易アプリを Firebase App Distribution に配布し、レンタルスマホ6台にアプリをインストールしました。
1画面だけのシンプルなアプリ
◼︎ユーザーの現在地
なるべくGoogleマップと同等の操作性が欲しいけど、こちらも通信量が膨大になるのが怖い...という事で、慎重に進めることとなりました。
初回の位置取得をgetCurrentPosition
で行い、その後はwatchPosition
で監視し続ける、という実装です。これにより、自然な動きの現在地更新を行うことができます。
// ユーザーの現在地表示、初回の位置取得
navigator.geolocation.getCurrentPosition(
handlePositionUpdate,
(error) => {
console.error('位置情報の取得に失敗しました:', error);
setGpsNotAvailable(true);
let errorMessage = '正確な現在地が取得できません';
if (error.code === 1) {
errorMessage = '位置情報の使用が許可されていません。ブラウザの設定を確認してください。';
} else if (error.code === 2) {
errorMessage =
'位置情報を取得できませんでした。GPSが有効になっているか確認してください。';
}
setLocationError(errorMessage);
toast.error(errorMessage, {
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
});
},
options
);
// 継続して位置を監視
watchId.current = navigator.geolocation.watchPosition(
handlePositionUpdate,
(error) => {
console.error('位置情報の取得に失敗しました:', error);
setGpsNotAvailable(true);
let errorMessage = '正確な現在地が取得できません';
if (error.code === 1) {
errorMessage = '位置情報の使用が許可されていません。ブラウザの設定を確認してください。';
} else if (error.code === 2) {
errorMessage =
'位置情報を取得できませんでした。GPSが有効になっているか確認してください。';
} else if (error.code === 3) {
errorMessage = '位置情報の取得がタイムアウトしました。もう一度お試しください。';
}
setLocationError(errorMessage);
toast.error(errorMessage, {
autoClose: 5000,
hideProgressBar: false,
closeOnClick: true,
pauseOnHover: true,
draggable: true,
});
},
options
);
}, [handlePositionUpdate]);
チャレンジと対策
◼︎駐車場・トイレ空き状況の報告機能
Googleマップでは何年か前から、交通規制や事故渋滞などを報告できる機能が搭載されています。
同じように、まさに今アプリを使用しているユーザーが、トイレや駐車場の空き状況を1タップで報告できるようにしたら、一箇所だけ大混雑!のようなイベントあるあるを避けられるのではないか。という事で、「空いてる」「混んでる」を各施設のアイコンから報告できる機能を実装しました。
現実シーンの想定(かつ悪意のあるユーザーへの対策)として、報告した空き状況は一定時間が経過したら自動的に削除されるよう設計しました。
ユーザー投票により、空きor混み状況が表示される
短納期・低予算・高品質!・・・
実は本件、とある事情で、非常にタイトな期間で開発せざるを得ませんでした。
タイトな納期とタイトな予算で、如何にして高パフォーマンスを出すか。どうすれば青柏祭に一番貢献できるのか。そんな課題を真正面から食らうと、デベロッパーとしてのプライドが掻き立てられ案外ワクワクするものでした。毎回これでは身体が持ちませんが。
地方在住のエンジニア苦悩話みたいになりましたが、地域の皆様にアプリやWEBサービスへの関心を高め、今以上に価値を感じてもらう。そうしてこそ私たちの持つ技術がこの先、この能登半島という地域で輝くと信じているから、今日も頑張るのです・・・
みなさま、GWは能登へ!
無事リリースが終わり、チラシのデザイン・印刷やプレスリリースも配信し終わったところでこの記事を書いております。
間違いなく、七尾市が一番盛り上がるタイミングです。
震災をきっかけに能登を知ったけど、行ったことがない!という方も是非、能登・七尾の青柏祭へお越しください。
Discussion