4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

MapLibre NativeでGeoJSONのLineStringを描画する (for iOS)

Last updated at Posted at 2023-10-15

この記事は、FOSS4G Japan 2023で発表した スマホアプリの地図表示における第三の選択肢 MapLibre Native を使ってみる の内容を書き起こしたものです。

概要

この記事は、OSSの地図ライブラリ MapLibre Native (for iOS) で、GeoJSONのLineString(線形データ)を描画してみようという記事です。

MapLibre Native のセットアップ方法については この記事を御覧ください。

用意するGeoJSON

今回は 国土交通省の国土数値情報(鉄道時系列データ) で公開されているGeoJSONデータを使用します。国土数値情報の元データは全国の路線データが格納されているため、QGISなどを使ってつくばエクスプレスの路線のみ抽出しています。

つくばエクスプレスの線形GeoJSON

このGeoJSONファイルを、Xcodeのプロジェクトの中に読み込ませます。色々とデータが入っていますが、今回は TX_Railway というファイルで格納しています。

ディレクトリ構造

GeoJSONを描画する

MapLibreNativeでGeoJSONの線形データを描画します。主な処理の流れは以下のとおりです。

  1. GeoJSONファイルを読み込む
  2. ソースを定義してMapViewに反映する
  3. レイヤーを定義する
  4. スタイルを編集する
  5. レイヤーをMapViewに反映する

GitHubのサンプルプログラム を見ていただくとわかりますが、MapLibreNativeはSwiftUIのViewとして用意されていないため、 UIViewRepresentable を継承している MapView を作成して、MapLibreNativeの地図を表示するViewを定義しています。その中で、上記の処理をそれぞれ書いていきます。

GeoJSONファイルを読み込む

最初にGeoJSONファイルを読み込む処理を書いていきます。

今回、GeoJSONファイルXcode内のプロジェクトに置いているため Bundle.main.url からファイルのURLを取得します。そのURLを使ってファイルの中身を Data 型に変換します。

MapView.swift
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の生成およびレイヤーの生成し、線形のスタイルを定義していきます。

MapView.swift
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が描画されたあとに実行する必要があるため UIViewRepresentableCoordinator で呼び出します。

Coordinator 緯度経度を定義するクラスではなく、 UIView における delegate (委任処理) を定義するために用意されている UIViewRepresentable の内部クラスです。

CoordinatorMGLMapViewDelegatemapViewDidFinishLoadingMap を呼び出します。この中で、新たに drawRailwayAndStation を呼び出しています。この drawRailwayAndStation で、GeoJSONを呼び出す loadGeoJSONData と、描画する loadGeoJSONData を呼び出しています。

MapView.swift
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アプリでも実装ができますので、ぜひお試しください。

4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?