🗺️

iOSでインドアマップ(屋内の地図)を表示する

2023/11/11に公開

iOSで、次のようにインドアマップ(屋内の地図)を地図上にオーバーレイ表示する機能があります。

scale=0.9

地図上にインドアマップの「画像」をオーバーレイすること自体は昔から可能だった[1]のですが、iOS 13以降から IMDF (後述)というフォーマットで記述された GeoJSON をMKGeoJSONDecoderというAPIでデコードし、MKMultiPolygon, MKMultiPolyline, MKMultiPolygonRenderer, MKMultiPolylineRendererといったAPIを使用してマップ上にオーバーレイできるようになりました。データフォーマットが規格化され、さらに描画APIが整備されたことにより、汎用的かつ統一感のあるインドアマップ表示が可能になったわけです。

Indoor Mapping Data Format(IMDF)

IMDFは、インドアマップを表現するためのデータフォーマットです。GeoJSON (RFC-7946)をベースとする2Dフォーマットで、ひとつのIMDFデータは複数の.geojsonファイルから構成されます。

scale=0.7

IDMFにはFootprint(建物の物理的範囲)、Level(各階/フロア)、Unit(部屋等)といったFeature Typesが定義されており、建物や屋外の施設を統一的フォーマットで規定できるようになっています。[2]

IMDFのデコード

MKGeoJSONDecoder

iOS 13で追加されたMKGeoJSONDecoderクラスを利用して、IMDFをデコードできます。

let data = try Data(contentsOf: fileURL)
let geoJSONDecoder = MKGeoJSONDecoder()
let geoJSONFeatures = try geoJSONDecoder.decode(data)
let features = geoJSONFeatures as! [MKGeoJSONFeature]

デコードが成功するとMKGeoJSONFeatureオブジェクトの配列が得られます。

MKGeoJSONFeature

MKGeoJSONFeatureクラスには次の3つのプロパティが定義されています。

var identifier: String? { get }
var properties: Data? { get }
var geometry: [MKShape & MKGeoJSONObject] { get }

propertiesプロパティにはシリアライズされたJSONデータが入っており、次のようにJSONDecoderを用いてデコードできます。

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
properties = try decoder.decode(Properties.self, from: feature.properties!)

またgeometryプロパティにはMKGeoJSONObjectプロトコルに準拠するMKShape型のジオメトリ情報が入っています。これは次の「描画」で使用します。

インドアマップの描画

IMDFをMKGeoJSONDecoderでデコードし、取得したMKGeoJSONFeatureオブジェクトのgeometryプロパティにはMKShapeを継承するオブジェクトが配列で入っていますが、試しにAppleのサンプルコード"Dinoseum"に入っているIMDFをデコードしてgeometryプロパティの中身を次のように出力してみると、

feature.geometry.forEach { shape in
    if let overlay = shape as? MKOverlay {
        print("\(overlay)")
    }
}

実はMKPolygonMKPolylineMKMultiPolygonといったMKOverlayプロトコルに準拠したクラスのオブジェクトが入っていることがわかります。

これらのMKOverlayに準拠するオブジェクト群を配列に入れて、MKMapViewaddOverlaysメソッドに渡すと、

mapView.addOverlays(overlays)

MKMapViewDelegatemapView(_:rendererFor:)が呼ばれるようになるので、MKOverlayプロトコルに準拠するそれぞれのクラスに応じたレンダラ(MK◯◯Renderer)を初期化して返します。

func mapView(_ mapView: MKMapView, 
                 rendererFor overlay: MKOverlay) -> MKOverlayRenderer {
    
    let renderer: MKOverlayPathRenderer
    switch overlay {
    case is MKMultiPolygon:
        renderer = MKMultiPolygonRenderer(overlay: overlay)
    case is MKPolygon:
        renderer = MKPolygonRenderer(overlay: overlay)
    case is MKMultiPolyline:
        renderer = MKMultiPolylineRenderer(overlay: overlay)
    case is MKPolyline:
        renderer = MKPolylineRenderer(overlay: overlay)
    default:
        return MKOverlayRenderer(overlay: overlay)
    }
       
    // レンダラの設定(strokeColor, lineWidth, fillColor, etc.)
       
    return renderer
}

scale=0.4

なお、MKMultiPolygon, MKMultiPolygonRendererと、MKMultiPolyline, MKMultiPolylineRendererはiOS 13で追加された新クラスです。それぞれ従来からあるMKPolygonMKPolylineの「マルチ」版です。IMDFのサポートに合わせ、複雑な描画をサポートするために導入されたものと考えられます。

脚注
  1. PDF形式のベクター画像を地図上にオーバーレイするサンプルがiOS 9の頃から公開されていた ↩︎

  2. Reference - Indoor Mapping Data Format ↩︎

Discussion