トップ地図アプリMap > 登録情報を表示する

登録情報を表示する

はじめに

公園、観光地などの案内図やバス時刻表のコピーなどpdfファイルや画像ファイルの名前には位置情報を含めている。 地図上にアイコンを表示し、このアイコンをタップすると、そのファイルを表示する。

タップの検出は写真撮影場所やバス停と同じため、行数が大きくなる場合には、共通化を図りたい。

Windows版ではファイル名は日本語でも問題はなかった。Androidエミュレータでは日本語ファイル名も正しく表示されるが、 スマホでは文字化けが起きる。 古くなっている情報も多い(特にバス時刻表で)ので、全面的に見直し、ファイル名は英字に変える。


*.pdf、*.mht ファイルの表示はWindows C#プログラムに比べて、はるかに面倒であることが分かった。

プログラム

情報登録およびアイコン表示

public class Information {
    final static String dirInfos = Map.DIR + "infos/";
    static Information[] infos;

    double lon, lat;
    String path;
    char type;

    public Information(double lon, double lat, String path, char type) {
        this.lon = lon;
        this.lat = lat;
        this.path = path;
        this.type = type;
    }

    static void drawInfos(Map map, Canvas canvas) {
        if (!loadInfos()) return;
        Rect srcTime = new Rect(0, 0, Map.bmpTimetable.getWidth(), Map.bmpTimetable.getHeight());
        Rect srcInfo = new Rect(0, 0, Map.bmpInformation.getWidth(), Map.bmpInformation.getHeight());

        for (Information info : infos) {
            Bitmap bmp = info.type=='T' ? Map.bmpTimetable : Map.bmpInformation;
            Rect src = info.type=='T' ? srcTime : srcInfo;
            int x = (int)(Map.Lon2X(info.lon, map.zoom)*Map.PX - (map.CX - map.W/2f));
            int y = (int)(Map.Lat2Y(info.lat, map.zoom)*Map.PX - (map.CY - map.H/2f));
            if (x < 0 || x > map.W || y < 0 || y > map.H) continue;
            Rect dst = new Rect((x-18), (y-18), (x+18), (y+18));
            canvas.drawBitmap(bmp, src, dst, null);
        }
    }

    public static boolean loadInfos() {
        if (infos != null) return true;  // 読み込み済み

        String[] files = new File(dirInfos).list();
        if (files == null) return false;

        List list = new ArrayList<>();
        for (String file : files) {
            if (!file.endsWith(".pdf") && !file.endsWith(".mht") &&
                    !file.endsWith(".jpg") && !file.endsWith(".png") && !file.endsWith(".gif")) {
                continue;
            }
            if (file.length() < 20) continue;
            String str = null;
            try {
                str = file.substring(file.length() - 20, file.length() - 4);
                String[] lonlat = str.split("[_T]");
                if (lonlat.length < 2) continue;
                double lon = Double.parseDouble(lonlat[0]);
                double lat = Double.parseDouble(lonlat[1]);
                list.add(new Information(lon, lat, file, str.charAt(8)));
            } catch (Exception ex) {
                System.out.printf("Error: %s[%s]\n", str, file);
                ex.printStackTrace();
            }
        }
        infos = list.toArray(new Information[0]);
        return true;
    }

}

タップされた情報の表示

    // 一番近い情報アイコンを探す
    static Information getTappedInformation(double lon, double lat) {
        double minDistance = Double.MAX_VALUE;
        Information minInfo = null;
        for (Information info : infos) {
            double dx = info.lon - lon;
            double dy = info.lat - lat;
            double distance = Math.sqrt(dx * dx + dy * dy);
            if (distance < minDistance && distance < 0.1) {
                minDistance = distance;
                minInfo = info;
            }
        }
        return minInfo;
    }

    static void showInformation(double lon, double lat) {
        Information info = getTappedInformation(lon, lat);
        if (info == null) return;
        System.out.printf("%s\n", info.path);
    }
                    double xt = (eX + (CX - W / 2.0)) / PX;
                    double yt = (eY + (CY - H / 2.0)) / PX;
                    if (bus_route) {
                        bus_route(xt, yt);
                    }
                    if (drawInformation) {
                        double lon = X2Lon(xt, zoom);
                        double lat = Y2Lat(yt, zoom);
                        Information info = Information.getTappedInformation(lon, lat);
                        if (info != null) {
                            //String url = "file://" + Map.DIR + "infos/" + info.path;
                            String url = "content://" + Map.DIR + "infos/" + info.path;
                            Intent intent = new Intent(Intent.ACTION_VIEW);
                            String extention = MimeTypeMap.getFileExtensionFromUrl(info.path);
                            String mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extention);
                            intent.setDataAndType(Uri.parse(url), mimetype);
                            try {
                                context.startActivity(intent);
                            } catch (Exception ex) {
                                ex.printStackTrace();
                            }
                        }
                    }
                }

Windows版情報登録プログラム

class Information {
    const string Dir = "d:/gisdata/infos";
    public string path;
    public double lon, lat;
    public int type;

    public Information(double lon, double lat, string path, int type) {
        this.lon = lon;
        this.lat = lat;
        this.path = path;
        this.type = type;
    }

    public static Information[] GetAllInformations() {
        List listInform = new List();
        string[] files = Directory.GetFiles(Dir);
        foreach (string path in files) {
            if (path.Length < 20) continue;
            try {
                string str = path.Substring(path.Length-20,16);
                string[] lonlat = str.Split(new char []{'_', 'T'});
                if (lonlat.Length < 2) continue;
                double lon = double.Parse(lonlat[0]);
                double lat = double.Parse(lonlat[1]);
                listInform.Add(new Information(lon,lat,path,str[8]));
            } catch {
                Console.Error.WriteLine(path);
            }
        }
        return listInform.ToArray();
    }
}

日本語ファイル名

ファイル名は日本語の方が分かりやすい。LDPlayer(Android 9)では問題がないが、実機(Android 12)ではエラーが起きることが多い。 例えばファイル名は「相原駅西口1 大戸行き139.33150T35.60624.pdf」であり、位置情報はファイル名末尾にある。 file.substring(file.length() - 22, file.length() - 4); で 139.33150T35.60624 を切り出そうとすると、 実機では、表示でも文字化けがあるが、正しい切り出しが行えない。

表示上の文字化けは構わないが、末尾の位置情報だけを正しく切り出したい。substring や file.length() は使わず、 ファイル名の末尾から位置情報を切り出せればよい。

String を byte配列に変換して、位置情報部分を切り出し、その byte配列を String に戻せはうまく行くかもしれない。

  byte[] sbyte = file.getBytes(StandardCharsets.ISO_8859_1);  // Shift-JIS
  byte[] locbyte = Arrays.copyOfRange(sbyte, sbyte.length-22, sbyte.length-4);
  String loc = new String(locbyte);

しかし、これでもダメだった。誤りが常に先頭文字に現れるので、暫定的に、次のようにした。

 if (str.charAt(0) != '1') str = "1" + str.substring(1); // 暫定

色んな文字に対して、'1' が 'T' に化けている。文字コード以外に何か問題があるのかもしれない。

A.リファレンス

[1] android.os.FileUriExusedException: file:///storage/emulated/0/test.txt が Intent.getData() を通じてアプリ外に公開されています
[2] Android標準のビューアでPDFを開きたい
[3] Android - FileProvider で外部アプリとファイルを共有する
[4] AndroidアプリからWordファイルを他のアプリで開き、編集・保存させたい

B.来歴およびノート

2023.10.16 エミュレータでは問題ないが、実機でエラー

    java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/2EBA-4DDF/Map/infos/nice139.50**_35.54**.gif
        at androidx.core.content.FileProvider$SimplePathStrategy.getUriForFile(FileProvider.java:825)
        at androidx.core.content.FileProvider.getUriForFile(FileProvider.java:450)
        at com.example.mapx.Map.onTouchEvent(Map.java:583)

エミュレータでは次のようになった。

content://com.example.mapx.fileprovider/external_files/Map/infos/nice139.50**_35.54**.gif

エミュレータではファイルをメインストレージに置いており、実機では SDカードに置いている。この違いがエラーに関係しているかもしれない。

エミュレータでは "/storage/emulated/0/Map/"、 実機では "/storage/2EBA-4DDF/Map/" である。

external-pathは次のようにしている。 external-path はメインストレージを意味するのかもしれない。files はcom.example.mapxの files サブディレクトリであろう。

    <external-path name="external_files" path="." />

実機の場合もファイルをメインストレージに置いた。これでエラーは取れた。SDカードを共有することも可能と思われるが、 当面は、情報ファイルをメインストレージに置くこととする。

 Information info = Information.getTappedInformation(lon, lat);
 if (info != null) {
     int index = info.path.lastIndexOf('.');
     String extention = info.path.substring(index + 1);
     String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extention);
     if (mime == null && extention.equals("mht")) mime = "multipart/related";
     File file = new File(DirInfo + "infos/" + info.path);
     Uri uri = FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".fileprovider", file);
     System.out.printf("%s\n%s\n", info.path, uri.toString());

     Intent intent = new Intent(Intent.ACTION_VIEW);
     if (mime == null) intent.setData(uri);  // このケースはない
     else intent.setDataAndType(uri, mime);

     intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);//2023.9.23
     try {
        context.startActivity(intent);
     } catch (Exception ex) {
        ex.printStackTrace();
     }
  }
[xml/paths]
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <cache-path name="files" path="." />
    <files-path name="document" path="document/" />
    <external-path name="external_files" path="." />
</paths>
[AndroidManifest.xml]
        <provider
            android:name="androidx.core.content.FileProvider"
            android:authorities="${applicationId}.fileprovider"
            android:exported="false"
            android:grantUriPermissions="true">
            <meta-data
                android:name="android.support.FILE_PROVIDER_PATHS"
                android:resource="@xml/paths" />
        </provider>

2023.9.24 日本語ファイル名に対応した

 
    String url = "content://" + Map.DIR + "infos/" + Uri.encode(info.path);
    System.out.printf("%s\n", url);
    Intent intent = new Intent(Intent.ACTION_VIEW);
   String extention = MimeTypeMap.getFileExtensionFromUrl(url);
    String mimetype = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extention);
    intent.setDataAndType(Uri.parse(url), mimetype);

2023.9.21 pdfファイルが表示されない

 
 Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
 context.startActivity(intent);
で、次のエラーが起きた。
    android.os.FileUriExposedException: file:///storage/emulated/0/Map/infos/jike139.4999_35.5664.pdf exposed beyond app through Intent.getData()

対策はネット検索で見つかった。

MainActivityの onCreate に以下を追加すればエラーは止まった。しかし、pdfファイルは開かない。ファイルマネージャでは開く。

  StrictMode.VmPolicy.Builder builder = new StrictMode.VmPolicy.Builder(); 
  StrictMode.setVmPolicy(builder.build());

Androidエミュレータでは 正常に表示されるようになった。しかし、スマホでは表示されない。


画像ファイルについてはエミュレータ、実機ともうまく表示されるようになった。pdfファイルについてはやはりエミュレータではうまくいくが、 実機ではダメ。

val target = Intent(Intent.ACTION_VIEW)
val uri = FileProvider.getUriForFile(requireContext(), "${BuildConfig.APPLICATION_ID}.fileprovider", pdfFile)
val mime = requireContext().contentResolver.getType(uri)

target.setDataAndType(uri, mime)
target.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION

startActivity(target)

file:// を content:// に置き換えれば動いた。