Posted at

FlutterでインラインMapViewを作ってみた(サンプルコード付き)

More than 1 year has passed since last update.


FlutterでMapViewのプロトタイプ

Flutterでマップを表示する為にプラグインを探してみても、インラインで表示することが出来ないという問題があります。GithubのIssuesを見ても3番目に:thumbsup:が多いのがGoogle Mapsのサポートです。コメント数で見てみるとダントツの一位ですね。

Screen Shot 0030-06-15 at 11.09.20.png

Google Mapsが使えない理由はFlutterの動作する仕組みの為で、別アクティブティを立ち上げて表示する分には使えますが、Flutterネイティブのインラインで好きなレイアウトに表示しようとすると難しいです。FlutterはSurfaceViewの上に全てのウィジェットを独自に描画しているというのがその大きな原因です。

Google Mapsをそのまま描画できなくても、描画に必要な地図データさえあれば、描画自体はそこまで難しくないのでは?という単純な甘い考えからオープンストリートマップの地図データを使ってやってみようと思い立ったのでその内容をまとめた記事がこちらです(前置きが長い)

オープンストリートマップ(OpenStreetMap)

https://www.openstreetmap.org/

Screen Shot 0030-06-15 at 11.20.08.png


地図データとは?

地図データには画像やフォントと同様に大きく2種類のデータがあります


  • ラスターデータ(jpg, png, bmp, gif...)

  • ベクトルデータ(ai, svg...)

058-2.jpg

(via マルチメディアスコーラ Chapter 2 - 東京情報大学)

地図サイトやアプリを見ていて、四角くタイル状に地図画像を描画されるサイトがあると思いますが、それらの多くが画像ファイルとして事前に描画して保存してあるデータをダウンロードして描画しています。

この場合、画像ファイルはサイズが大きくなることが多く、ダウンロードにも時間がかかり、挙動としてももっさりしていて使いにくいことが多いです。メリットとしてはクライアント側の性能が悪くても描画できるということです。昔のガラケーなどではこちらの方式で表示されていました。

スマホ全盛の現在では、画面スワイプでスクロールして動かすためにタイル形式だとダウンロードも間に合わず、転送量も多くなってしまい。デメリットが大きいです。逆にスマホ端末はCPU等の性能が高い為、描画処理自体は問題になりませんので、現在のスマホ向けではベクトル形式のデータをダウンロードしてクライアントサイドで描画することが多いです。

今回も同様にベクトルデータを取得してクライアントサイドで描画する地図を作ろうと思います。


シェープファイル

シェープファイル (Shapefile) は、 他の地理情報システム(GIS)間でのデータの相互運用におけるオープン標準として用いられるファイル形式である。[1] 例えば、井戸、川、湖などの空間要素がベクタ画像である点 (数学)、線分、多角形で示され、各要素に固有名称や温度などの任意の属性を付与できる

Wikipedia: シェープファイル

375px-Simple_vector_map.svg.png


シェープファイルのダウンロード

http://download.geofabrik.de/

OpenStreetMapのデータを各地域ごとにダウンロード出来るようにしているサイトです

こちらからAsia -> Japan -> Kantoと辿って関東のデータ(kanto-latest-free.shp.zip)をダウンロードします

http://download.geofabrik.de/asia/japan/kanto-latest-free.shp.zip

READMEによると毎日同じURLから最新版のデータが更新されるそうです。

Screen Shot 0030-06-15 at 12.31.22.png


シェープファイルの読み込み

Flutterで使うためにファイルを読み込む必要がありますがPluginを探してもシェープファイルを読み込むためのプラグインは見つかりませんでした(探し方が悪いだけかも)。

今回の目的はシェープファイルを読み込むことではないのでまずは地図描画部分を作るためにjsonファイルにコンバートしたデータを読み込むことにします。


GeoJSON

GeoJSON[1]はJavaScript Object Notation (JSON) を用いて空間データをエンコードし非空間属性を関連付けるファイルフォーマットである。 属性にはポイント(住所や座標)、ライン(各種道路や境界線)、 ポリゴン(国や地域)などが含まれる

Wikipedia: GeoJSON

シェープファイルがバイナリなのに比べてGeoJSONはライブラリも豊富なテキスト形式(Json)です。今回はこちらのGeoJSONフォーマットを使うことにします


シェープファイルのコンバート

QGISを使ってシェープファイルをGeoJSONとしてエクスポートします

QGIS

https://qgis.org/ja/site/

QGISはオープンソースの地理情報を扱うためのGUIツールです。地図データを扱う人はみんな使っているツールですが、地図のデータを扱わない人にとってはおそらく初めて聞くツールですね。

Screen Shot 0030-06-15 at 13.36.27.png

↑はお台場付近の道路情報のみをQGISに読み込ませて描画したもの

gis_osm_roads_free_1.cpg

gis_osm_roads_free_1.dbf
gis_osm_roads_free_1.prj
gis_osm_roads_free_1.shp
gis_osm_roads_free_1.shx

のファイルをQGISにドラッグするとあとはQGISが描画してくれます。

シェープファイルを読み込んだあと、QGISのメニューからLayer -> Save Asで以下のダイアログが出てくるのでフォーマットにGeoJSONを選択して保存場所を指定する

Screen Shot 0030-06-15 at 13.47.24.png

で吐き出されたgeojsonの中身は以下のような感じになります

{

"type": "FeatureCollection",
"name": "gis_osm_roads_free_1",
"crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } },
"features": [
{ "type": "Feature", "properties": { "osm_id": "4847506", "code": 5111, "fclass": "motorway", "name": "首都高速11号台場線", "ref": "11", "oneway": "F", "maxspeed": 0, "layer": 3, "bridge": "T", "tunnel": "F" }, "geometry": { "type": "MultiLineString", "coordinates": [ [ [ 139.7578, 35.6437952 ], [ 139.7578196, 35.6417602 ], [ 139.7578327, 35.6406704 ], [ 139.757831, 35.639852 ], [ 139.7578397, 35.6395924 ], [ 139.7578542, 35.639448 ], [ 139.7578741, 35.6393004 ], [ 139.7579045, 35.6391582 ], [ 139.7579335, 35.6390562 ], [ 139.757966, 35.6389645 ], [ 139.7580079, 35.6388687 ], [ 139.7580599, 35.6387672 ], [ 139.7581094, 35.6386844 ], [ 139.7581707, 35.6385969 ], [ 139.7582349, 35.6385161 ], [ 139.7583132, 35.6384262 ], [ 139.7583976, 35.6383438 ], [ 139.75849, 35.6382657 ], [ 139.758599800000013, 35.6381829 ], [ 139.7587212, 35.638103 ], [ 139.7588227, 35.6380454 ], [ 139.7589356, 35.637996 ] ] ] } },

... 略 ...

全部そのままエクスポートしたのでサイズが700MBを超過していましたので、まずは描画周りのテストを行うため、はじめの5000行のみを使うことにしました(30MB弱)

間引いたGeoJSONをQGISに読み込ませてみたのが以下

Screen Shot 0030-06-15 at 14.14.00.png

地理的な位置をGeoJSONの配列における位置は何の関連性もないようで、良く分からない感じになっていますが、まずはこれでFlutter側の実装を進めていくことにします。

場所によってはちゃんと道路を判別可能

Screen Shot 0030-06-15 at 14.17.28.png


CustomPaintでCanvasに描画

事前に_roadsPointsにはGeoJSONから読み込んだLatLngを入れておく

Widget

List<Offset> _roadsPoints = <Offset>[];

GestureDetector(
child: CustomPaint(
painter: CustomMap(points: _roadsPoints),
size: Size.infinite
) // CustomPaint
) // GestureDetector

CustomMap

class CustomMap extends CustomPainter {

List<Offset> points;

CustomMap({this.points});

@override
void paint(Canvas canvas, Size size) {
Paint paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 3.0;

for (int i = 0; i < points.length - 1; i++) {
if (points[i] != null && points[i + 1] != null) {
canvas.drawLine(points[i], points[i + 1], paint);
}
}
}

@override
bool shouldRepaint(CustomMap oldDelegate) => oldDelegate.points != points;
}

Screenshot_20180615-200015.png

とりあえず、GeoJSONから読み込んだ地図データをFlutterのWidgetに描画すると、こんな状態になりました。

背景色、ボーター色、道路の色を指定した状態がこちら↓ 少し地図らしくなってきましたね。

Screenshot_20180615-202019.png


道路情報以外のデータも読み込んで描画

まずOffsetの点群(_tmpPoints)をPathに変換

Path path = Path()..addPolygon(_tmpPoints, true);

drawPathにpathを渡してビル群を描画

canvas.drawPath(path, paint);

道路と合わせて建物も描画したのがこちら。単純に描画するだけなら色を決めて並べるだけなので簡単ですね。

Screenshot_20180618-112200.png


ズーム出来るようにする

GestureDetectorを使ってZOOMレベルを取得

GestureDetector(

onScaleUpdate: (ScaleUpdateDetails details) {
setState(() {
_scale = details.scale;
});
},

以下のようにcanvasのscaleメソッドを使って描画しているものを全て同時に拡縮出来ます

canvas.scale(scale, scale);

以下が実行結果

ezgif-1-1661ee4356.gif


スクロール出来るようにする

GestureDetectorを使って移動距離を取得して保存

Offset _delta = Offset.zero;

... 略 ...
GestureDetector(
onPanUpdate: (DragUpdateDetails details) {
setState(() {
_delta = _delta + details.delta;

translateでcanvasを移動

canvas.translate(delta.dx, delta.dy);

以下が実行結果

ezgif-4-e02637b497.gif


まとめ

以上のように地図の元データをOpenStreetMapから取得することで、Flutterネイティブで地図を描画し、拡縮、移動などMapViewの基礎的な機能を実装することが出来ました。プラットフォームのAPIは使用していないのでiOS,Android両OSで動作します(シミュレーターで確認済み)

Screen Shot 0030-06-21 at 2.54.21.png

今回特定のエリア(渋谷駅近辺)のデータだけを使って試していますので、変換済みのGeoJSONをアプリ内に埋め込んで実行しています。広いエリアに対応するには描画に必要なエリアの地図データをサーバー経由で取得するようにするなど、地図としてまともに動作するまでにはやるべきことが山積みですが、第一歩としては悪くないと思っています。


ソースコード

https://github.com/aoinakanishi/flutter-openstreetmap-example