Mapbox for iOSで自作のスタイルを設定する(好きな背景図を表示する)
はじめに
地図系のフレームワークで有名で、最近ではゼンリンとの提携がニュースになったりしたMapboxはiOS向けにもフレームワークを提供しています。
Mapbox for iOSの導入
ただ地図を表示するだけなら、チュートリアルのとおりとてもシンプルに書けます。
import Mapbox class ViewController: UIViewController { override func viewDidLoad() { super.viewDidLoad() let url = URL(string: "mapbox://styles/mapbox/streets-v11") //Mapbox公式スタイル let mapView = MGLMapView(frame: view.bounds, styleURL: url) mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight] mapView.setCenter(CLLocationCoordinate2D(latitude: 59.31, longitude: 18.06), zoomLevel: 9, animated: false) view.addSubview(mapView) } }
コメントにちょっと書いてありますが、このフレームワークは基本的に公式スタイルを表示するように作られています。スタイルは.json形式のデータにも関わらずです。 構造的には公式スタイルを参照するのが最もラクですが、そのためにはAPIトークンが必要で、アクセス数に応じて料金が発生します。というのが面倒臭いわけです。 では、OpenStreetMapなどオープンデータを活用し、公式スタイルを使わなければ良いのではないか?という事で色々試しました。
Mapbox公式以外のスタイルを適用する
Mapboxのスタイルは、MGLMapViewの初期化時にstyleURLという引数で設定されます。 つまりURLを渡さなければなりませんが、どこかのサーバーにデータを置いておく、というのはナンセンスなので、ローカルで完結させたいです。 という訳で、①好きなデータ等を設定したスタイルを.json形式で作成する、②①のローカルアドレスを取得しMGLMapViewに渡す、という方針になりました。
①:スタイルを.json形式で作成する
Mapboxのスタイルのデータ構造は、Swift辞書としては以下のとおり表現できます。
private var style:[String:Any] = [ "version":8, "sources":[], "layers":[], ]
versionは8で固定です。sourcesはレイヤーデータの形式等の設定、layersはsourcesのデータをどのように表示するか(色など)の設定となっています。 つまりsourcesとlayersに適切な初期データを与えて辞書をつくり、.json形式で出力すれば良いわけです。
そんなわけで、Mapboxのスタイルに関する機能をひとまとめにしたMapStyleManagerというクラスを実装しました。
import Foundation struct MapStyleManager { private var style:[String:Any] = [ "version":8, "name":"basemap", "sources":[], "layers":[], ] mutating func applyDefault() { let defaultName = "国土地理院オルソ" let defaultTileUrl = "https://cyberjapandata.gsi.go.jp/xyz/seamlessphoto/{z}/{x}/{y}.jpg" let defaultAttributionUrl = "https://maps.gsi.go.jp/development/ichiran.html" self.setBasemap(name: defaultName, tileUrlStr: defaultTileUrl, attributionUrl:defaultAttributionUrl) } func getStyle() -> [String:Any] { return self.style } mutating func setStyle(styleDict:[String:Any]) { self.style = styleDict } mutating func setBasemap(name:String, tileUrlStr:String, attributionUrl:String="", tileSize:Int=256) { self.style["name"] = name let sources = [ name:[ "type":"raster", "tiles":[tileUrlStr], "attribution":"<a href='" + attributionUrl + "'>" + name + "</a>", "tileSize":tileSize ] ] let layers = [ [ "id":name, "type":"raster", "source":name, "minzoom":0, "maxzoom":18 ] ] self.style["sources"] = sources self.style["layers"] = layers } func readJson(inputDir:String, filename:String) -> [String:Any] { let nsHomeDir = NSHomeDirectory() let inputPath = nsHomeDir + inputDir + "/" + filename + ".json" guard let jsonData = try? Data(contentsOf: URL(fileURLWithPath: inputPath) ) else { preconditionFailure("Failed to load JSON file") } guard let jsonObj = try? JSONSerialization.jsonObject(with: jsonData, options: []) else { preconditionFailure("Failed to parse JSON file") } let jsonDict = jsonObj as! [String:Any] return jsonDict } //By serializing styleDic(class prop, Swift Dictionary), WRITE .JSON. func writeJson(outputDir:String, filename:String) -> URL? { let nsHomeDir = NSHomeDirectory() let outputPath = nsHomeDir + outputDir + "/" + filename + ".json" do { let jsonData = try JSONSerialization.data(withJSONObject: self.style, options: .prettyPrinted) try jsonData.write(to: URL(fileURLWithPath: outputPath)) return URL(fileURLWithPath: outputPath) } catch { print("error") return nil } } }
このクラスはクラス変数として常にひとつのstyleを持ち、前述のメソッドなどでそのstyleを操作します。 styleは、各種クラスメソッドでのみ読み書き可能です。
各コードを解説します。 まず望みのスタイルを作成します。
mutating func setBasemap(name:String, tileUrlStr:String, attributionUrl:String="", tileSize:Int=256) { let sources = [ name:[ "type":"raster", "tiles":[tileUrlStr], "attribution":"<a href='" + attributionUrl + "'>" + name + "</a>", "tileSize":tileSize ] ] let layers = [ [ "id":name, "type":"raster", "source":name, "minzoom":0, "maxzoom":18 ] ] self.style["sources"] = sources self.style["layers"] = layers }
上記のコードは、望みのラスタータイルのURLなどを与えることで、Mapboxスタイル形式の辞書を作成するメソッドです。 レイヤーのnameとタイルのtileUrlStrを与えるとスタイルを作成します。 あとはこの辞書を.jsonファイルとして出力してやります。
func writeJson(outputDir:String, filename:String) -> URL? { let nsHomeDir = NSHomeDirectory() let outputPath = nsHomeDir + outputDir + "/" + filename + ".json" do { let jsonData = try JSONSerialization.data(withJSONObject: self.style, options: .prettyPrinted) try jsonData.write(to: URL(fileURLWithPath: outputPath)) return URL(fileURLWithPath: outputPath) } catch { print("error") return nil } }
この関数では、Swiftの辞書をJSON形式の文字列に変換し.jsonファイルとして出力します。 writeJson()は実行されるたびに、iOS内の/tmpにstyle.jsonを出力しそのURLを返します。/tmpは、アプリを終了した後のデータの保存は保証されない、一次保存用フォルダですので、今回のような用途にもってこいですね。
②:①のローカルアドレスを取得しMGLMapViewに渡す
①のローカルアドレスは、writeJson()の返り値です。
①、②を踏まえて、自前のスタイルを設定する場合、以下のとおり書けます。
var msManager = MapStyleManager() msManager.applyDefault()//デフォルトは国土地理院オルソ let tmpStyleUrl = msManager.writeJson(outputDir: "/tmp", filename: "style") mapView = MGLMapView(frame: rect, styleURL: tmpStyleUrl!)
さて、これで純正スタイルを設定する必要がなくなりました。 API tokenを削除してみましたが、問題なく動作します。API tokenはMapboxスタイルにアクセスするためのものでした。 つまりアクセス数による課金だとか、そういった恐れがなくなります。
サンプル
国土地理院データを表示してみたサンプル