卒研の説明を受けた後、研究内容をどうするか吉田先生と話し合った。とりあえず伊島のアプリの経路を選択するアルゴリズムを研究することにした。プロコンにも出したいので、資料も作る必要がある。
地図アプリに使われているアルゴリズムについて調べた。最短経路問題を解くアルゴリズムのダイクストラ法が使われていることがわかった。どういうアルゴリズムか理解することができた。 ダイクストラ法の説明
課題がわかりやすいほうがいい
ダイクストラ法をjsで実装してみた。Clude3.7の力を借りて実装した。
Gitで地図機能を改良するための新しいブランチを作って、アプリをエミュレーターで動かそうとしたが、起動中で止まってしまい、起動できなかった。
ThinkPadにAndroidStudioを入れて、伊島のアプリを動かすことができた。昨日動かなかった原因はGitHubからクローンした後に、gradleの同期ができていなかったからということがわかった。
どの時間のフェリーで帰るかを選択すると、現在の時刻とフェリーの時刻から逆算しておすすめの経路を提案してくれる。自分の身体能力を設定できるようにする。研究の評価は、GoogleMapやYamapが提案するルートと、伊島のアプリが提案するルートを比較する。実際に使ってもらって評価してもらう。
過去のプロコンの資料を参考にしながら、作成した。機能を箇条書きで書いた。
プロコンの資料を優先的に作って、月ゼミでプロコンの資料のことを発表する。
yamapやgoogleMapなどの類似ステムを調べた。 離島の人口の推移などを調べた。 国土交通省の離島の資料
マイナンバーカードや免許証で本人確認をしたアプリに、避難できたかどうかを確認する機能を付ければ確実。LINEやYouTubeなどの誰もがインストールしているアプリにその機能が付けば実用的。
システム構成図を作成した。
MapLibreはオープンソースの地図描画ライブラリで、OSM(OpenStreetMap)の地図データを表示することができることがわかった。実際に動かしてみるために、AndroidStudioの依存関係に
implementation ("org.maplibre.gl:android-sdk:11.8.7")
を追加して、layoutフォルダに
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.maplibre.android.maps.MapView
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
を書き込んで使おうとしたが、アプリがストップして開くことができなかった。
使えない道を選択して道案内できる 人によってエッジの重みを変える 地図を作る機能で難易度を設定できるように 避難所とフェリー乗り場までの時間を表示
神戸のベクタータイルデータ 試しに神戸の地図データ(pbf)をダウンロードして、オフラインで表示しようとしたが、エラーが出て、表示されない。
2025-07-02 10:55:03.163 10697-10738 Mbgl com.example.maplibretest E {RenderThread 81}[Style]: Failed to load tile 11/1792/813=>11 for source vector-tiles: unknown pbf field type exception
コードやダウンロードした地図データを確認したが特に問題は見当たらなかった。ネット上を探して問題を見つけていく。無理そうだったら、別の地図データを使ってアプリを作ってみる。
MapLibreのドキュメント 公式ドキュメントを見てみたが、APIの説明をしているだけで、よくわからなかった。そのためGitHubで参考になりそうなプロジェクトを探すことにした。
map-libre-demo 探した結果、このプロジェクトが見つかった。Ramani MapsというMap LibreのAndroid向けのラッパーを使って、オフラインで地図を表示している。このプロジェクトを参考にオフライン地図を作っていく。
オフラインで地図を表示するために必要な手順をまとめる。Android Studioで新しいプロジェクトを作成したら、build.gradle.ktsに次の依存関係を追加する。
implementation("org.ramani-maps:ramani-maplibre:0.7.0")
そして、map-libre-demoのMainAcctivity.ktとMapStyleManager.ktを作成する。その後、app/src/mainにassetsフォルダを作成し、プロジェクトの中身をコピペする。 これでアプリを起動すると、マルタの地図がオフラインで表示される。 別の地図をオフラインで表示させたい場合
companion object {
private const val STYLE_FILENAME = "style.json"
private const val MBTILES_FILENAME = "Kobe.mbtiles"
private const val FILE_URI_PLACEHOLDER = "___FILE_URI___"
}
日本の地図データ このサイトで日本の地図データをダウンロードすることができる。しかし、このままでは容量が1.83GBもあるため、必要な場所だけを切り出す必要がある。 必要な場所を切り出すには、Dockerをインストールした後に、切り出したい.mbtilesがあるディレクトリで次のコマンドを実行すればできる。(Windowsの場合)
docker run --rm -v "%cd%":/data openmaptiles/openmaptiles-tools mbtiles-tools copy /data/tiles.mbtiles /data/extract.mbtiles --reset --auto-minmax --bbox=...
OpenMapTiles Toolを使っている。
Ramani Mapsで地図上にピンを立てるには、MapLibre()を呼び出すときのコールバック関数にSymbol()を渡して、その中でいろいろと設定する。
MapLibre(
modifier = modifier,
styleBuilder = styleBuilder,
cameraPosition = cameraPosition
) {
// 現在地が取得できたらマーカーを表示
currentLocation?.let { loc ->
Symbol(
center = LatLng(loc.latitude, loc.longitude),
size = 0.1f,
color = "#00AAFF",
isDraggable = false,
imageId = R.drawable.images // 任意のマーカー画像
)
}
ピンの位置は次のように設定する。
center = LatLng(緯度,軽度)
今回は2秒ごとに位置情報を取得して、反映させるようにした。
まず、地図データは .mbtilesという拡張子。阿南の地図データなら、Anan.mbtilesという感じ。ここにデータを追加したいが、このままだとバイナリ形式なので、読み書きが難しい。そこで、一度 .geojsonというJSONのような拡張子に変換する。そこでデータを編集した後に、再度 .mbtilesに戻せばよい。下は阿南高専を示すGeoJSONの例。
{ "type": "Feature", "id": 3145034231, "properties": { "class": "school", "name": "徳島県立阿南工業高等学校", "name:ja": "徳島県立阿南工業高等学校", "name:latin": "tokushimaken ritsu anan kougyou koutougakkou", "name:nonlatin": "徳島県立阿南工業高等学校", "name_de": "徳島県立阿南工業高等学校", "name_en": "徳島県立阿南工業高等学校", "name_int": "tokushimaken ritsu anan kougyou koutougakkou", "rank": 1, "subclass": "school" }, "geometry": { "type": "Point", "coordinates": [ 134.639559, 33.925179 ] } }
このためにtippecanoeというツールを使う。
tippecanoe
試しに、GeoJSONに変換して地点を追加して、元に戻そうとしたが、元に戻すときに1時間以上かかってしまい戻すことができなかった。
阿南の地図データに点を追加、Anan.mbtiles → GeoJSON → mbtiles をしようとすると、うまくいかなかった。調べたところ、このような変換は推奨されていないことがわかった。Mbtilesにはいくつものレイヤーがあるが、この変換をしてしまうと、そのレイヤーが1つになってしまう。
そこで、新しいMbtilesを作って、Anan.mbtilesに重ねて表示する方法を試した。下のようなnew_points.geojsonというファイルを使って、阿南高専の近くに新しい地点を追加してみた。
{
"type": "Feature",
"id": 9999999999,
"properties": {
"class": "college",
"name": "Test Point",
"name:ja": "テスト地点",
"rank": 1,
"subclass": "college"
},
"geometry": {
"type": "Point",
"coordinates": [134.667983, 33.897995]
}
}
しかし、地図上には新しい地点が表示されることはなかった、、、変換に時間がかかることもなく、エラーも出なかった。
何が原因なのか予想がつかないので、ネット上でうまくいっている事例を探して、原因を見つけていく。
避難所と観光地点と港でここに来たボタンを押せるようにした。gpsの現在位置の距離が何Mに近づいたらボタンを有効化する、という設定をした。
ユーザーが追加した観光ポイントがアプリに反映されるようにした。サーバからGeoJsonを取得して、それを描画している。 使用する関数を別のファイルに分けた。data/GeoJsonPoiParser.kt と data/PoiRepository.kt というファイルを作成した。GeoJsonPoiParser はjsonをkotlinのPOI(地図上の特定のポイント)オブジェクトに変換するための関数をまとめたもの。 PoiRepository は POI の情報をサーバから取得して、キャッシュに保存するもの。 これらの関数を地図を描画する際に呼び出して、観光ポイントを更新するようにした。
現在地に戻るボタン、POI,ルートの更新ボタンを実装した。ETag というものを使い、データに変更があった場合のみ取得して、保存するようにしようとしたが、毎回保存されてしまう。サーバ側でGASを使っているのが問題だと思う。
阿南市と松江市を合体させたルーティングファイルで、伊島のデータが壊れていた。伊島に新しい道を追加した状態で、松江市のデータと合体させるときに壊れたようだ。初期状態の阿南市と松江市のデータを合体させた後に、新しい道を追加するようにしたら、この問題は解決した。
新しい道には ID や、どこの地点同士をつなぐかなどの情報が入っておらず、Python で自動補完する仕組みになっている。これが原因で合成が上手くいかなかったのだろう。
MapStyleManager の setupStyle関数の引数で、使用する地図の名前を受け取って、それを適用するようにした。これをassets内から読み取って使用している。
次は、ユーザーがボタンを押して指定した地図をsetupStyle関数の引数に渡して、それを描画するようにしたい。その後、地域ごとにディレクトリを作成して、その地域のデータをまとめておくようにしたい。最後にサーバーと連携できるようにする。
アプリ内にボタンを配置して、表示する地図を切り替えられるようにした。阿南市と、松江市の地図を切り替えられることが確認できた。
次のような selectedMbtilesName という状態付き変数を定義した。ボタンを押すとこの値が変化し、地図が再描画される。
var selectedMbtilesName by remember {
mutableStateOf("anan_minami_tokushima.mbtiles")
}
MapScreen.ktという地図を表示させているファイルが1000行を超えてきて、どういった処理をしているのかわかりづらくなってきた。そこで、機能ごとにファイルを分割してわかりやすくしようと思った。
Kotlin でのアプリ開発では View Model というクラスを作って機能を分割する。ViewModel は、画面に必要な状態(State)と、その状態を更新するための処理をまとめて管理するクラスである。要するに、11/12 のノートで書いたような状態付き変数とボタンを押したときの処理のようなものを別ファイルに書くということである。
試しに MenuSheet というメニュー画面で使用している状態付き変数とボタンを押した後の動作を View Model の別ファイルに移動させた。 MapScreenViewModel.kt というファイルを作り、ここにまとめた。これからもViewModelのほうにコードを移行させていき、保守管理しやすくしていきたい。
年末までにアンケートをやる。年明けから論文を書いていく感じ。
実際にアプリを使ってもらうにあたって、今のままでは地点の追加しかできないため不便だ。間違って追加した地点を消したり、編集したりすることができない。そこで削除編集できるようにする。
今はさくらインターネットのサーバーで地図関連のapiを動かしている。そのままでは https 通信ができないので、GAS をサーバとアプリの間に置くことで https 通信を実現している。しかしこの状態では1度に通信できる容量に制限があったり、json形式でしか送れないため、画像をBASE64で送り、デコードする必要がある。そこで、サーバーもさくらのものではなく、無料のホスティングサービスを利用して、 https 通信できるようにする。
いくつかサービスを比較してみた結果、apiの配信先として Vercel 、データベースとして Supabase を使用することにした。どのサービスも同じようなことができたが、使ったことがあり、人気だったので選んだ。
アプリの全取得URLを Vercel のものに変更したが、アプリにPOIのピンが表示されない。おそらく、 ID が UUID になっているのが原因だと思う。
geojson をいくつかのパターンで試して原因を突き止めた。geojsonの最後にカンマ , があるときと、idが数字(int) ではなくuuid(String)になっているとき表示できなかった。
解決策として、今までの数字のidをregionSeqとして持ち、主キーにuuidを使用することにした。uuidはPostgresSQLで標準的に使われているIDであるし、将来的にオフラインで新しいPOIを一時保存できるようにしたとき、衝突が起こらないので採用することにした。
また、regionId という値に ishima のように地域名を持つようにした。以下はgeojsonのスキーマである。
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"id": "uuid-string",
"geometry": {
"type": "Point",
"coordinates": [longitude, latitude]
},
"properties": {
"id": "uuid-string",
"regionId": "string",
"regionSeq": number,
"name": "string",
"category": "string | null",
"description": "string | null",
"images": ["string"],
"props": {},
"created_at": "ISO8601 string",
"updated_at": "ISO8601 string"
}
}
]
}
properties.id と Feature.id はがあるが、正しいのは properties.id であり、これを信用して使用する。 Feature.id は地図描画ライブラリで使用する可能性があるため、念のため持っている。