前回は、地図上に現在地を表示して軌跡を表示させることをやりました。
alasixosaka.hatenablog.com
新アプリもようやく形になってきました。
今回は、GPXファイルを読み込んで表示することをやります。昔のアプリについていたPOIの表示もついでに行います。POIと言っても中身はGPXファイルでやっていることはほぼ同じなのでまとめてやってしまいます。GPXファイル、POIファイルの作り方は最後に書きます。
今回はほぼ昔のアプリのコピペで済んでいますが、ソースコードが非常に長いのでその点はご勘弁ください。これでも昔のアプリに比べてだいぶ短くしたつもりです。
今回も変更点のみを記載します。変更するのは、レイアウト(activity_main.xml)、MainActivity.javaとRotation.javaです。
activity_main.xml
レイアウトファイルですが、ほぼ昔のアプリのスタイルに戻しました。ただし、ボタンを一つ減らしています。上から順番に、マップファイルの選択、GPX/POIファイルの選択、地図の表示と3つボタンが並んでその下にラジオボタンが縦に2つ並んでいます。一番下がテーマの選択です。ラジオボタンとテーマ選択は今回はまだ使いません。昔のアプリと変わっているところは、GPXファイルの選択とPOIファイルの選択をまとめて1つのボタンにしたことです。詳細はMainActivityのところで説明します。
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/btMapfileSelect"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/mapfileselect"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.498"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.144" />
<Button
android:id="@+id/btGPXSelect"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="@string/gpx_file_select"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.502"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.265" />
<Button
android:id="@+id/btRotationView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/RotationView"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.387" />
<Button
android:id="@+id/btSelectTheme"
android:layout_width="200dp"
android:layout_height="wrap_content"
android:text="@string/select_theme"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.502"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/radiogroup"
app:layout_constraintVertical_bias="0.57" />
<RadioGroup
android:id="@+id/radiogroup"
android:layout_width="350dp"
android:layout_height="wrap_content"
android:background="#df7401"
android:orientation="horizontal"
android:paddingLeft="10dp"
android:paddingTop="10dp"
android:paddingRight="10dp"
android:paddingBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.508"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.552">
<RadioButton
android:id="@+id/rbOn"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginLeft="25dp"
android:layout_marginRight="25dp"
android:layout_weight="1"
android:background="#ffffff"
android:text="@string/onGPS" />
<RadioButton
android:id="@+id/rbOff"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_marginLeft="25dp"
android:layout_marginRight="25dp"
android:layout_weight="1"
android:background="#ffffff"
android:text="@string/offGPS" />
</RadioGroup>
<RadioGroup
android:id="@+id/radiogroup2"
android:layout_width="350dp"
android:layout_height="80dp"
android:background="#df7401"
android:orientation="horizontal"
android:paddingLeft="10dp"
android:paddingTop="15dp"
android:paddingRight="10dp"
android:paddingBottom="0dp"
app:layout_constraintBottom_toTopOf="@id/btSelectTheme"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.508"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/radiogroup"
app:layout_constraintVertical_bias="0.242">
<RadioButton
android:id="@+id/rb2On"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="25dp"
android:layout_marginRight="25dp"
android:layout_weight="1"
android:background="#ffffff"
android:text="@string/on" />
<RadioButton
android:id="@+id/rb2Off"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginLeft="25dp"
android:layout_marginRight="25dp"
android:layout_weight="1"
android:background="#ffffff"
android:text="@string/off" />
</RadioGroup>
<TextView
android:id="@+id/heightMap"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/heightMap"
android:textColor="#3F51B5"
android:textSize="18sp"
android:textStyle="bold"
app:layout_constraintStart_toStartOf="@id/radiogroup2"
app:layout_constraintTop_toTopOf="@id/radiogroup2"
tools:ignore="MissingConstraints" />
</androidx.constraintlayout.widget.ConstraintLayout>メニュー画面はこんな感じになります。

ActivityMain.java
メインアクティビティはGPX/POIファイル選択ボタンのクリック動作に対する処理を追加します。
昔のアプリでは、ファイルダイアログというのを表示して選択するようにしていました。こちらのサイトを参考にしていました。
www.hiramine.com
ところがこれもコードが古くエラーになるので、面倒なのでマップファイルを選択するときと同様にファイルマネージャーを使って指定するようにしました。ですので、処理の中身はマップファイルの選択と全く同じことを行っています。なので、実はマップファイル選択のボタンをタップしても同じ結果が得られます。ボタンをわざわざ分けているのは、マップファイルとGPX/POIファイルの選択では利用するシーンが異なるためボタンを分けているだけです。選択したファイルの区別は拡張子で行っています。すなわち拡張子が.mapなら地図ファイル、.gpxならGPXファイル、.poiならPOIファイルというように区別しています。
まず、clickWait()のところに、下記のようにGPX/POI選択のボタンを押したときの処理を追記します。
private void clickWait(){
//ボタンのクリック待ち。
Button btClick = findViewById(R.id.btRotationView);
~追記する~
Button btMapfileSelect = findViewById(R.id.btMapfileSelect);
Button btGPX = findViewById(R.id.btGPXSelect);
~終わり~
BtListener listener = new BtListener();
btClick.setOnClickListener((View.OnClickListener) listener);
btMapfileSelect.setOnClickListener((View.OnClickListener)listener);
~追記する~
btGPX.setOnClickListener(listener);
~終わり~
}実際の処理はonClickに書きます。上にも書いたようにやっていることはマップファイル選択の時と全く同じです。
public void onClick(View view) {
int id = view.getId();
SharedPreferences preferences;
if (id == R.id.btRotationView) {
Intent intent = new Intent(getApplication(), Rotation.class);
startActivity(intent);
} else if (id == R.id.btMapfileSelect) {
preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
MAP_FILE = preferences.getString(fillter, "berlin.map");
Path = preferences.getString("path", sdPath);
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("file/*");
activityResultLauncher.launch(intent);
~追記する~
} else if (id == R.id.btGPXSelect) {
preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
Path = preferences.getString("path", sdPath);
fillter = "gpx";
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
intent.setType("file/*");
activityResultLauncher.launch(intent);
~終わり~
}ファイル選択の結果はActivityResultLancherに帰ってきますのでここを選択したファイルの拡張子を見て処理を分けるように変更します。
private final ActivityResultLauncher<Intent> activityResultLauncher = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
if (result.getData() != null) {
~省略~
if (ext.equals("map")){
SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("map", mapfile);
editor.putString("path", fpath);
editor.apply();
~追記~
} else if (ext.equals("gpx")) {
SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("gpx", mapfile);
editor.putString("path", fpath);
editor.apply();
} else if (ext.equals("poi")) {
SharedPreferences preferences = getSharedPreferences("DATA", Context.MODE_PRIVATE);
SharedPreferences.Editor editor = preferences.edit();
editor.putString("poi", mapfile);
editor.putString("path", fpath);
editor.apply();
~終わり~
~省略~
Rotation.java
次に地図を表示するRotation.javaです。
まず、変数類として下記を追加します。latLongXは、POIのマーカーを表示するために使う座標で初期位置はどこでも構いません。
GPX_FILEとPOI_FILEはそれぞれGPXファイルとPOIファイル解析用の変数。Pathはファイルのパス格納用。sdPathは外部ファイル格納ディレクトリの初期パスを入れています。基本は不要ですが、SharedPreferencesを読みだすときに空のときに返す値として入れています。AtName、AtValue、XPname、POInameはGPXファイル/POIファイル解析用の変数。lati、longiはGPX/POIファイルから読みだした経度緯度を格納する用。Olati、Olongiは距離を計算するときに使用しています。GPXsはGPXから読みだした値を格納するための配列。Tdistanceは計算で求めたコースのトータル距離を格納するため、elvはGPXから読みだした高度を格納するためのもの。elvmaxはルート上の最高高度を格納します。
protected LatLong latLongX = new LatLong( 34.859881, 135.577572);
private static String GPX_FILE = "";
private static String POI_FILE = "";
private static String Path ="";
String sdPath = Environment.getExternalStorageDirectory().getPath();
private String AtName;
private String AtValue;
private String XPname;
private String POIname;
private double lati = 0;
private double longi = 0;
private double Olati = 0;
private double Olongi = 0;
ArrayList<Double> GPXs = new ArrayList<>();
private double Tdistance = 0;
private double elv = 0;
private double elvmax=0;
createLayers()
createLayersにGPXとPOIを表示するために下記を追記します。addOberlayLayersの引数としてmapViesのレイヤーを渡しています。addOverlayLayersの中身この下に書きます。
protected void createLayers() {
~省略~
mapView.getTouchGestureHandler().setRotationEnabled(true);
~追記~
addOverlayLayers(mapView.getLayerManager().getLayers());
~終わり~
~省略~
addOverlayLayers
上で呼び出しているaddOverlayLayers関数です。これはサンプルプログラムのOverlayMapView.javaにあった関数を少しアレンジして使っています。長いので少しずつ分けて説明します。
まず変数類の定義です。
ofsetについては、昔のアプリでアンドロイドのバージョンが変わった時にマーカーの位置がずれていたのでそれを修正するために使っていました。今のところゼロで問題ないようですが、一応将来のために残してあります。それから、SharedPreferencesからGPXファイル、POIファイルのファイル名と格納されているディレクトリを読み込んでいます。GPXファイルとPOIファイルは地図ファイルと同じディレクトリに置いてある必要があります。
polylineはGPXのルート描画用の変数。latlongsはPOIのマーカー表示用の変数です。
protected void addOverlayLayers(Layers layers) {
int ofset =0;
int ofset2 = 0;
SharedPreferences preferences = getSharedPreferences("DATA", MODE_PRIVATE);
GPX_FILE = preferences.getString("gpx", "null.gpx");
POI_FILE = preferences.getString("poi", "null.poi");
Path = preferences.getString("path", sdPath);
Polyline polyline = new Polyline(createPaint(
AndroidGraphicFactory.INSTANCE.createColor(Color.BLUE),
(int) (4 * mapView.getModel().displayModel.getScaleFactor()),
Style.STROKE), AndroidGraphicFactory.INSTANCE);
List<LatLong> latLongs = new ArrayList<>();
FileInputStream is = null;
ArrayList<Marker> markers = new ArrayList<Marker>();
List<LatLong> rdis = new ArrayList<>();次がGPXファイルの解析です。
まず、sharedpreferencesで読み込んだGPXファイルを開いてxppに読み込みます。
eventTypeでSTART_TAGが来たらその後が必要な中身です。
実はGPXファイルには色々な情報が書き込まれてるのですが、その中から各ポイントの緯度、経度、高度を取り出しています。高度は高低図を表示するために使っています。今回は高低図は表示しませんので要りませんが、後々高低図を表示するときに使うので処理として行っています。
AtNameに各要素の名前、AtValueにその値が入りますので、AtNameがlatなら緯度、lonなら経度ということなので、それぞれlatlongsに追記します。また、GPXsにも追記します。GPXsは残り距離の計算に使います。
直前のポイントの緯度、経度がOlati、Olongiに入っていますので、それと現在ポイントの緯度、経度からポイント間の距離を計算し、Tdistanceに加算していって、各ポイントのスタート地点からの距離としてGPXsに追記しています。
次が残り距離の計算です。GPXsには、緯度、経度、高度、スタートからの距離の4つが各ポイントごとに入っていますので、サイズを4で割ってポイント数を算出しroopに代入しています。restは残り距離を格納する変数です。
ポイントはお尻から読みだして、各ポイントのスタートからの距離をGPXsから読みだして、トータル距離と比較します。最初はrestは1ですので、トータル距離から各ポイントのスタートからの距離を引いた値がrestより大きくなるポイントが残り1㎞地点です。この地点の緯度、経度をrdisに追記します。この処理を残り10㎞まで繰り返しています。
最後にpolyline.setPoints(latLongs); として各ポイントを一つの線としてセットしています。
//GPXファイルの解析
try {
String listXmlPath = Path + "/"+ GPX_FILE;
is = new FileInputStream(new File(listXmlPath));
BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
//XMLファイルをまとめて読み込み
XmlPullParser xpp = Xml.newPullParser();
xpp.setInput(reader);
//解析するXMLファイルの中身を渡す
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case XmlPullParser.START_DOCUMENT:
Log.i("MainActivity", "ドキュメント開始");
break;
case XmlPullParser.START_TAG:
XPname = xpp.getName();
Log.i("MainActivity", XPname + "要素開始");
int attrCount = xpp.getAttributeCount();
for (int i = 0; i < attrCount; ++i) {
AtName = xpp.getAttributeName(i);
AtValue = xpp.getAttributeValue(i);
Log.i("MainActivity", " " +
i + "番目の属性 = " + xpp.getAttributeName(i));
Log.i("MainActivity"," " +
i + "番目の値 = " + xpp.getAttributeValue(i));
if(AtName.equals("lat")){
lati = parseDouble(AtValue);
GPXs.add(lati);
}
if( AtName.equals("lon")) {
longi = parseDouble(AtValue);
GPXs.add(longi);
if(Olati+Olongi!=0) {
float[] distance =
getDistance(Olati, Olongi, lati, longi);
Tdistance += distance[0]/1000;
}
Olati=lati;
Olongi=longi;
GPXs.add(Tdistance);
LatLong latLong = new LatLong(lati, longi);
latLongs.add(latLong);
}
}
break;
case XmlPullParser.TEXT:
String t = xpp.getText();
Log.i("MainActivity", "テキスト = " + t);
if (XPname.equals("ele")){
elv = parseDouble(t);
if (elv>elvmax){
elvmax = elv;
}
GPXs.add(elv);
XPname="";
}
break;
case XmlPullParser.END_TAG:
Log.i("MainActivity", xpp.getName() + "要素終了");
break;
}
eventType = xpp.next();
//次のトークンに進む
}
Log.i("MainActivity", "ドキュメント終了");
} catch (XmlPullParserException e) {
Log.e("MainActivity", "XMLの解析失敗.");
} catch (IOException e) {
Log.e("MainActivity", "XMLファイルの読み込みに失敗.");
}
//残り距離の計算
int roop = GPXs.size()/4;
int rest = 1;
if (Tdistance>=1){
for (int i = roop-1; i>=0; i--){
double dis = GPXs.get(i*4+2);
if ((Tdistance-dis)>=rest){
lati = GPXs.get(i*4);
longi = GPXs.get(i*4+1);
LatLong latLong = new LatLong(lati,longi);
rdis.add(latLong);
rest++;
}
if ((rest>10) || (rest>=Tdistance)){
break;
}
}
}
polyline.setPoints(latLongs);次がPOIファイルの読み出しです。
まずDrawable、Bitmap、Markerの3つを定義しています。それから配列の定義が2つ続きます。POIlistの方は、POIファイルに入っている各POIの固有の記号です。POIとしては、右折、左折の記号が各8個、給水所、レストラン、トイレ、それからそれ以外の注意ポイントを表示するための旗の合計20種類のマークを使います。それぞれのマークのdrawableが次のmakarListに入ります。これらの記号は、パワーポイントで作ったり、ネットから拾ってきたりした画像を使っています。AndroidStudioのASSETを使って作成することもできると思います。ただし、矢印に関しては向きに応じて8方位の矢印が必要なので、一旦ファイルをダウンロードして画像処理ソフトなどで画像を回転させてあげて、方向別に8種類のファイルを作成する必要があります。
次はファイルの読み出しです。ファイル形式はGPXと同じなので同じ処理をして中身を読みだしています。ポイントの緯度、経度はlatlongXに書き込んでいます。
下の方のif (XPname.equals("icon")){ 以下のところでマーカーを決めています。各POIにはiconというキーワードがついています。
こんな感じです。
<kashmir3d:icon>956005</kashmir3d:icon>
そこに並んでいる数字が固有の記号になるので、POIlistのどこに入っているかをindexOfで探して、要素の位置をindexに返します。見つからなければ-1が帰ります。indexが-1でない場合は、要素の位置から、一致するdrawableを使って、poimarker = createMarker(markerList[index], latLongX, ofset); としてmarkerを作成します。作ったマーカーはmarkersに書き加えていきます。
//POIファイルの解析
Drawable drawable;
Bitmap bitmap;
Marker poimarker;
List POIlist = new ArrayList<>(Arrays.asList ("1001009", "1001010", "1001011", "1001012","1001013", "1001014", "1001015",
"1001016", "1001001", "1001002", "1001003", "1001004", "1001005", "1001006", "1001007", "1001008",
"952010", "956005", "909037", "951003"));
//List list = new ArrayList<>(Arrays.asList("s", "a", "m", "u", "r", "a", "i"));
int markerList [] = {R.drawable.left1, R.drawable.left2, R.drawable.left3, R.drawable.left4,
R.drawable.left5, R.drawable.left6, R.drawable.left7, R.drawable.left8, R.drawable.right1,
R.drawable.right2, R.drawable.right3, R.drawable.right4, R.drawable.right5, R.drawable.right6,
R.drawable.right7, R.drawable.right8, R.drawable.toilet3, R.drawable.restrun3, R.drawable.water4,
R.drawable.flag2};
try {
String listXmlPath = Path + "/"+ POI_FILE;
is = new FileInputStream(new File(listXmlPath));
BufferedReader reader = new BufferedReader(new InputStreamReader(is, "UTF-8"));
//String content = new Scanner(
// new File(listXmlPath)).useDelimiter("\\z").next();
//XMLファイルをまとめて読み込み
XmlPullParser xpp = Xml.newPullParser();
xpp.setInput(reader);
//解析するXMLファイルの中身を渡す
int eventType = xpp.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT) {
switch (eventType) {
case XmlPullParser.START_DOCUMENT:
Log.i("MainActivity", "ドキュメント開始");
break;
case XmlPullParser.START_TAG:
XPname = xpp.getName();
Log.i("MainActivity", XPname + "要素開始");
int attrCount = xpp.getAttributeCount();
for (int i = 0; i < attrCount; ++i) {
AtName = xpp.getAttributeName(i);
AtValue = xpp.getAttributeValue(i);
Log.i("MainActivity", " " +
i + "番目の属性 = " + xpp.getAttributeName(i));
Log.i("MainActivity"," " +
i + "番目の値 = " + xpp.getAttributeValue(i));
if(AtName.equals("lat")){
lati = parseDouble(AtValue);
}
if( AtName.equals("lon")) {
longi = parseDouble(AtValue);
latLongX = new LatLong(lati, longi);
}
}
break;
case XmlPullParser.TEXT:
POIname = xpp.getText();
Log.i("MainActivity", "テキスト = " + POIname);
if (XPname.equals("icon")){
int index = POIlist.indexOf(POIname);
if (index != -1) {
poimarker = createMarker(markerList[index], latLongX, ofset);
markers.add(poimarker);
}
}
case XmlPullParser.END_TAG:
Log.i("MainActivity", xpp.getName() + "要素終了");
break;
}
eventType = xpp.next();
//次のトークンに進む
}
Log.i("MainActivity", "ドキュメント終了");
} catch (XmlPullParserException e) {
Log.e("MainActivity", "XMLの解析失敗.");
} catch (IOException e) {
Log.e("MainActivity", "XMLファイルの読み込みに失敗.");
}残り距離の表示。基本はさっきのPOIと同じです。残り距離が1㎞~10㎞まで1㎞ごとにrdisに入っていますので、それを取り出して、各ポイントのマーカーを作成してmarkersに追記しています。各ポイントのマークはR.drawable.oneからR.drawable.tenまでにはいっていますので、これをdistanceMarkという配列に入れています。
int count =1;
int rdismax = rdis.size();
int distanceMark[] = {R.drawable.one, R.drawable.two, R.drawable.tre, R.drawable.fou, R.drawable.fif,
R.drawable.six, R.drawable.sev, R.drawable.eig, R.drawable.nin, R.drawable.ten};
while((count<11)&&(count<rdismax)) {
poimarker = createMarker(distanceMark[count-1], rdis.get(count-1), ofset2);
markers.add(poimarker);
count++;
}この辺の処理は昔のアプリでは、一つ一つswitch caseで処理を書いていましたが、配列を使ってかなりすっきした処理に書き換えました。
最後はレイヤーの追記です。GPXのルートの線(polyline)と各POI、残り距離のマーク(markers)をレイヤーに追記して表示できるようにします。
layers.add(polyline);
for (int i=0; i < markers.size(); i++ ){
layers.add(markers.get(i));
}
}長かったですがaddOverlayLayersはこれで終わりです。
createMarker
上のaddOverlayLayersで使っているcreateMarker関数です。サンプルプログラムではUtils.javaに入っているものをそのままコピペしています。
private Marker createMarker(int resourceIdentifier, LatLong latLong, int ofset) {
Bitmap bitmap = new AndroidBitmap(BitmapFactory.decodeResource(this.getResources(), resourceIdentifier));
return new Marker(latLong, bitmap, ofset, ofset);
}
getDistance
残り距離を計算するのに使っている関数です。経度、緯度から2点間の距離を計算しています。
public float[] getDistance(double x, double y, double x2, double y2) {
// 結果を格納するための配列を生成
float[] results = new float[3];
// 距離計算
Location.distanceBetween(x, y, x2, y2, results);
return results;
}
GPXファイル、POIファイルの作り方
GPXファイルに関しては、私の場合は、ガーミンコネクトやRide with GPSなどのウェブサイトを用いて作成しています。登山などの時にはらくルート(旧ヤマプラ)を使うこともあります。これらのサイトは地図上で地点をクリックしていくと自動的にルートが作成できるのでルート作成は非常に楽です。
POIファイルについてはカシミール3Dというソフトを使っています。POIと言っていますが、カシミール3Dではウェイポイントという呼び方をしています。また、mapsforgeにもPOIの機能がありますが、このアプリではmapsforgeのPOIは使っていません。ですので、ちゃんとしたPOIというわけではありません。mapsforgeやガーミンの地図などにはPOIとして多数の地減が登録されていて、ガーミンなどではそこまでのルートを自動で作成し、ナビゲーションするといった機能がついています。mapsforgeでも同様のことができるようですが、自分の場合は、あまり使わないので実装していません。そういった用途であればグーグルマップで十分事足りると考えています。
カシミール3Dでは地図上の任意の点を右クリックし、新規ー>ウェイポイントの作成 からウェイポイントを作成することができます。アプリで使っているレストラン、給水、フラッグなどはカシミール3Dに最初から組み込まれているガーミンGPSのアイコンを使っています。また、矢印に関してはオリジナルアイコンを作成して登録しています。オリジナルアイコンの作成については下記を参考にしてください。
www.kashmir3d.com
また、私の過去記事も参考になります。
alasixosaka.hatenablog.com
カシミール3Dでウェイポイントを作成し、編集ー>GPSデータ編集 を選んでGPS編集画面を表示し、GPSデータの下にあるウェイポイントをダブルクリックするとウェイポイントの一覧が見れます。ここでウェイポイントの一覧から出力したいウェイポイントを選択し、ファイルー>選択したGPSデータの書き出し を実行するとウェイポイントがGPXデータとして書き出すことができます。このファイルの拡張子gpxをエクスプローラーなどでpoiに書き換えて使っています。
エミュレータで表示してみました。

全体が分かるようにちょっとズームアウトしたところを表示していますので、トイレと給水のマークが重なっている感じになっていますが、GPXのコース、POI、残り距離がちゃんと表示されています。