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


:::note info
この記事は、FOSS4G Japan 2023で発表した [スマホアプリの地図表示における第三の選択肢 MapLibre Native を使ってみる](https://speakerdeck.com/kyotonagoya/foss4g-japan-2023-sumahoapurinodi-tu-biao-shi-niokerudi-san-noxuan-ze-zhi-maplibre-native-woshi-tutemiru) の内容を書き起こしたものです。
:::

## 概要

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

:::note info
MapLibre Native のセットアップ方法については [この記事を御覧ください。](https://qiita.com/k15015kk/items/6b9ca9fca2f659665050)
:::

:::note info
[GitHubにサンプルプログラムを置いています。](https://github.com/k15015kk/TXMapSample)
:::

## 用意するGeoJSON

今回は [国土交通省の国土数値情報(鉄道時系列データ)](https://nlftp.mlit.go.jp/ksj/gml/datalist/KsjTmplt-N05-v1_3.html) で公開されているGeoJSONデータを使用します。国土数値情報の元データは全国の路線データが格納されているため、QGISなどを使ってつくばエクスプレスの路線のみ抽出しています。

![つくばエクスプレスの線形GeoJSON](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/8c58189e-b009-6a8d-6206-3772c7498d1d.png)

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

<img width="240px" alt="ディレクトリ構造" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/4814f095-4cd1-bd31-0c58-99ba00500a01.png">

## GeoJSONを描画する

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

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

[GitHubのサンプルプログラム](https://github.com/k15015kk/TXMapSample) を見ていただくとわかりますが、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のドキュメントで解説されています。](https://docs.mapbox.com/ios/maps/api/6.4.1/predicates-and-expressions.html)

## Coordinatorに追記

上記で定義したGeoJSONファイルを読み込む処理と、Mapに描画する処理を呼び出します。地図上に線形描画をするためには、MapViewが描画されたあとに実行する必要があるため `UIViewRepresentable` の  `Coordinator` で呼び出します。

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

`Coordinator` で `MGLMapViewDelegate` の `mapViewDidFinishLoadingMap` を呼び出します。この中で、新たに `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` の中で描画処理を呼び出します。

## シュミレーターで実行

ここまでで一度シュミレーターを通してアプリを実行してみると、地図上につくばエクスプレスの線形データが表示されます。（もちろん実機デバイスを繋げて動かしても大丈夫です。）

<img width="360px" alt="マップ上に線形が表示されている" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93706/8ebb6131-dba4-e11a-fd33-4f576b638a97.png">


## おわりに

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