この記事は、FOSS4G Japan 2023で発表した スマホアプリの地図表示における第三の選択肢 MapLibre Native を使ってみる の内容を書き起こしたものです。
概要
この記事は、OSSの地図ライブラリ MapLibre Native (for iOS) で、GeoJSONのLineString(線形データ)を描画してみようという記事です。
MapLibre Native のセットアップ方法については この記事を御覧ください。
用意するGeoJSON
今回は 国土交通省の国土数値情報(鉄道時系列データ) で公開されているGeoJSONデータを使用します。国土数値情報の元データは全国の路線データが格納されているため、QGISなどを使ってつくばエクスプレスの路線のみ抽出しています。
このGeoJSONファイルを、Xcodeのプロジェクトの中に読み込ませます。色々とデータが入っていますが、今回は TX_Railway というファイルで格納しています。

GeoJSONを描画する
MapLibreNativeでGeoJSONの線形データを描画します。主な処理の流れは以下のとおりです。
- GeoJSONファイルを読み込む
- ソースを定義してMapViewに反映する
- レイヤーを定義する
- スタイルを編集する
- レイヤーをMapViewに反映する
GitHubのサンプルプログラム を見ていただくとわかりますが、MapLibreNativeはSwiftUIのViewとして用意されていないため、 UIViewRepresentable
を継承している MapView
を作成して、MapLibreNativeの地図を表示するViewを定義しています。その中で、上記の処理をそれぞれ書いていきます。
GeoJSONファイルを読み込む
最初にGeoJSONファイルを読み込む処理を書いていきます。
今回、GeoJSONファイルXcode内のプロジェクトに置いているため Bundle.main.url
からファイルのURLを取得します。そのURLを使ってファイルの中身を Data
型に変換します。
func loadGeoJSONData(resourceName: String) async -> Data {
guard let = Bundle.main.url(forResource: resourceName, withExtension: "geojson") else {
preconditionFailure("GeoJSONファイルの読み込みに失敗しました")
}
guard let jsonData = try? Data(contentsOf: jsonURL) else {
preconditionFailure("GeoJSONファイルのパースに失敗しました")
}
return jsonData
}
線形を描画する処理を定義
次にGeoJSONの線形データを使ってマップに描画する処理を書きます。先程の手順の 2~5 の部分をここで書きます。
先程読み込んだGeoJSONの Data
を引数として受け取り、それを使ってShapeの生成およびレイヤーの生成し、線形のスタイルを定義していきます。
func drawRailway(_ mapView: MGLMapView, geoJson: Data) {
// MGLMapViewのStyleがあるか確認
guard let style = mapView.style else {
return
}
// GeoJSONデータからShapeを生成
guard
let shapeFromGeoJson = try? MGLShape(data: geoJson, encoding: String.Encoding.utf8.rawValue)
else {
fatalError("MGLShapeの生成ができませんでした")
}
// 表示ソースを定義
let shapeSource = MGLShapeSource(identifier: "railway-source", shape: shapeFromGeoJson, options: nil)
style.addSource(shapeSource)
// レイヤーを定義
let lineLayer = MGLLineStyleLayer(identifier: "railway-line-style", source: shapeSource)
// 始点・終点の形
lineLayer.lineJoin = NSExpression(forConstantValue: "round")
lineLayer.lineCap = NSExpression(forConstantValue: "round")
// 線の色
lineLayer.lineColor = NSExpression(forConstantValue: UIColor.orange)
// 線の幅
// ズームレベルに応じて幅を変えたい場合 mgl_interpolate:withCurveType:parameters:stops: を使って定義します
lineLayer.lineWidth = NSExpression(
format: "mgl_interpolate:withCurveType:parameters:stops:($zoomLevel, 'linear', nil, %@)",
[10: 2.0, 18: 8.0]
)
// Viewにレイヤーを追加
style.addLayer(lineLayer)
}
線の幅を mgl_interpolate:withCurveType:parameters:stops:
を使って動的に変更しています。この関数はMapBoxのSDKに存在していて、MapLibreNativeにフォークされたあとも引き継がれているようです。ズームレベルに応じて、線の太さを変えています。上記のソースコードでは、ズームレベルが10のときに線の幅を2、ズームレベルが18のときに線の幅を8とするように、線形補間 ( linear
) でズームレベルに応じて線の幅を変えています。
この mgl_interpolate:withCurveType:parameters:stops:
を含む関数の解説は、 Mapboxのドキュメントで解説されています。
Coordinatorに追記
上記で定義したGeoJSONファイルを読み込む処理と、Mapに描画する処理を呼び出します。地図上に線形描画をするためには、MapViewが描画されたあとに実行する必要があるため UIViewRepresentable
の Coordinator
で呼び出します。
Coordinator
緯度経度を定義するクラスではなく、 UIView
における delegate
(委任処理) を定義するために用意されている UIViewRepresentable
の内部クラスです。
Coordinator
で MGLMapViewDelegate
の mapViewDidFinishLoadingMap
を呼び出します。この中で、新たに drawRailwayAndStation
を呼び出しています。この drawRailwayAndStation
で、GeoJSONを呼び出す loadGeoJSONData
と、描画する loadGeoJSONData
を呼び出しています。
func makeCoordinator() -> MapView.Coordinator {
Coordinator(self)
}
class Coordinator: NSObject, MGLMapViewDelegate {
var control: MapView
init(_ control: MapView) {
self.control = control
}
func mapViewDidFinishLoadingMap(_ mapView: MGLMapView) {
self.control.drawRailwayAndStation(mapView)
}
}
func drawRailwayAndStation(_ mapView: MGLMapView) {
Task {
let railwayData = await loadGeoJSONData(resourceName: "TX_Railway")
await MainActor.run {
drawRailway(mapView, geoJson: railwayData)
}
}
}
読み込み処理は async
で宣言されているため Task
の中で await
を付ける必要があります。また、描画処理は MainThread で実行する必要があるため await MainActor.run
の中で描画処理を呼び出します。
シュミレーターで実行
ここまでで一度シュミレーターを通してアプリを実行してみると、地図上につくばエクスプレスの線形データが表示されます。(もちろん実機デバイスを繋げて動かしても大丈夫です。)

おわりに
今回は、MapLibreNativeを使ってGeoJSONファイルから線形データを描画する処理を紹介しました。iOSアプリベースで紹介していますが、もちろんAndroidアプリでも実装ができますので、ぜひお試しください。