Mapbox Maps SDK for iOSの逆引き辞典
Mapbox Maps SDK for iOS を使った各種実装方法をまとめていきます。(公式のExamplesページ やリポジトリに同梱されているサンプルアプリを参照しつつ自分なりに噛み砕いてまとめたものになります)
地形を3D表示する
-
StyleURI
にsatelliteStreets
を使用
mapView = MapView(frame: view.bounds, mapInitOptions: .init(styleURI: .satelliteStreets))
-
Terrain
を追加-
RasterDemSource
の作成 →Terrain
の作成 →MapboxMap
にsetTerrain
(親クラスであるStyleManager
のメソッド) -
Terrain
のexaggeration
プロパティ: この値を乗算することで、地形の標高を誇張する。 初期値:1
、 値の範囲:[0, 1000]
-
var demSource = RasterDemSource(id: "mapbox-dem")
demSource.url = "mapbox://mapbox.mapbox-terrain-dem-v1"
// Setting the `tileSize` to 514 provides better performance and adds padding around the outside
// of the tiles.
demSource.tileSize = 514
demSource.maxzoom = 14.0
try! mapView.mapboxMap.addSource(demSource)
var terrain = Terrain(sourceId: "mapbox-dem")
terrain.exaggeration = .constant(1.5)
try! mapView.mapboxMap.setTerrain(terrain)
別パターン 1
-
StyleURI
にはstreets
を使用
let options = MapInitOptions(cameraOptions: camera, styleURI: .streets)
mapView = MapView(frame: view.bounds, mapInitOptions: options)
-
Terrain
に加え、hillshade
タイプのレイヤーを追加することで陰影をつける-
RasterDemSource
を再利用してhillshade
タイプのレイヤーも追加
-
var demSource = RasterDemSource(id: "mapbox-dem")
demSource.url = "mapbox://mapbox.mapbox-terrain-dem-v1"
demSource.tileSize = 512
demSource.maxzoom = 14.0
try! mapView.mapboxMap.addSource(demSource)
var terrain = Terrain(sourceId: demSource.id)
terrain.exaggeration = .constant(0.5)
try! mapView.mapboxMap.setTerrain(terrain)
// Re-use terrain source for hillshade
let properties = [
"id": "terrain_hillshade",
"type": "hillshade",
"source": demSource.id,
"hillshade-illumination-anchor": "map"
] as [ String: Any ]
try! mapView.mapboxMap.addLayer(with: properties, layerPosition: .below("water"))
別パターン 2
- カスタムスタイルを使用
if let url = URL(string: "mapbox://styles/mapbox-map-design/ckhqrf2tz0dt119ny6azh975y") {
styleURI = StyleURI(url: url)
}
let mapInitOptions = MapInitOptions(cameraOptions: cameraOptions, styleURI: styleURI ?? .satelliteStreets)
mapView = MapView(frame: view.bounds, mapInitOptions: mapInitOptions)
-
Terrain
を追加-
hillshade
レイヤーはなし
-
コード
var demSource = RasterDemSource(id: "mapbox-dem")
demSource.url = "mapbox://mapbox.mapbox-terrain-dem-v1"
demSource.tileSize = 514
demSource.maxzoom = 14.0
try! mapView.mapboxMap.addSource(demSource)
var terrain = Terrain(sourceId: demSource.id)
terrain.exaggeration = .constant(1.5)
do {
try mapView.mapboxMap.setTerrain(terrain)
} catch {
print("Failed to add a terrain layer to the map's style.")
}
建物を3D表示する
実装方法を見る前に、この "Extrusion" と呼ばれる建物を3D化する手法の「しくみ」について理解した方がコードを理解しやすいと思う: https://www.docswell.com/s/shu223/KN1RGE-GIS-iOS#p67
コードはこちら:
var layer = FillExtrusionLayer(id: "3d-buildings", source: "composite")
layer.minZoom = 15
layer.sourceLayer = "building"
layer.fillExtrusionColor = .constant(StyleColor(.lightGray))
layer.fillExtrusionOpacity = .constant(0.6)
layer.filter = Exp(.eq) {
Exp(.get) {
"extrude"
}
"true"
}
layer.fillExtrusionHeight = .expression(
Exp(.get) {
"height"
}
)
layer.fillExtrusionBase = .expression(
Exp(.get) {
"min_height"
}
)
layer.fillExtrusionVerticalScale = .expression(
Exp(.interpolate) {
Exp(.linear)
Exp(.zoom)
15
0
15.05
1
}
)
layer.fillExtrusionAmbientOcclusionIntensity = .constant(0.3)
layer.fillExtrusionAmbientOcclusionRadius = .constant(3.0)
try! mapView.mapboxMap.addLayer(layer)
結構長いが、FillExtrusionLayer
を作成し、マップに追加しているだけ。
しくみを理解した上でコードを読むと、地図のデータソースにある "building" レイヤーに対して、
- "extrude" フィールドを "true" に設定
- "height" フィールドの値を
FillExtrusionLayer
のfillExtrusionHeight
プロパティにセット - "min_height" フィールドの値を
FillExtrusionLayer
のfillExtrusionBase
プロパティにセット
ということをやっているのがしっくり腹落ちする。
カスタムスタイルを使用する
Blueprintスタイル
MapInitOptions
を使う
-
styleJSON
引数を使う(styleURI引数よりこちらが優先される)
let jsonURL = Bundle.main.url(forResource: ..., withExtension: "json")!
let styleJSON = try String(contentsOf: jsonURL)
let options = MapInitOptions(styleJSON: styleJSON)
-
styleURI
引数を使う
let localStyleURL = Bundle.main.url(forResource: "blueprint_style", withExtension: "json")!
let styleURI = .StyleURI(url: localStyleURL)!
let options = MapInitOptions(styleURI: StyleURI(url: localStyleURL))
MapboxMap
クラスのプロパティを使う
実際には親クラスの StyleManager
のプロパティ
mapView.mapboxMap.styleJSON = styleJSON
mapView.mapboxMap.styleURI = styleURI
現在位置を示す Puck の表示
2D
let configuration = Puck2DConfiguration.makeDefault(showBearing: true)
mapView.location.options.puckType = .puck2D(configuration)
3D
人間のモデルが3D Puck(ダックのモデルは後述する ModelLayer`)
let uri = Bundle.main.url(forResource: "model", withExtension: "glb")
let model = Model(uri: uri, orientation: [0, 0, 180])
let configuration = Puck3DConfiguration(
model: model,
modelScale: .constant([50, 50, 50]),
modelOpacity: .constant(0.5),
layerPosition: .default
)
mapView.location.options.puckType = .puck3D(configuration)
mapView.location.options.puckBearing = .course
mapView.location.options.puckBearingEnabled = true
進行方向に追従させる
ビューポート(表示領域)を常に現在位置から進行方向を見た状態に追従させるには、 FollowPuckViewportState
を使用する:
let followPuckViewportState = mapView.viewport.makeFollowPuckViewportState(
options: FollowPuckViewportStateOptions(
bearing: .heading))
mapView.viewport.transition(to: followPuckViewportState)
現在位置取得やトラッキングのコードを書く必要もない。
addLayer
する階層を指定する
FillLayer
や LineLayer
を単に addLayer
してしまうと、
try mapView.mapboxMap.addLayer(landuseLayer)
try mapView.mapboxMap.addLayer(roadLayer)
こんな感じで Puck の上にレイヤーが描画されてしまったりする。
これを回避するには、addLayer
の layerPosition
引数に LayerPosition
を指定する:
try mapView.mapboxMap.addLayer(landuseLayer, layerPosition: .below("road-label"))
try mapView.mapboxMap.addLayer(roadLayer, layerPosition: .above("road-label"))
LayerPosition
の定義は以下のようになっていて、.at
でインデックスを直接指定できるほか、.above
や .below
で Layer
の id
を指定して相対位置を指定できる。
`LayerPosition` の定義
/// Specifies the position at which a layer will be added when using `Style.addLayer`.
public enum LayerPosition: Equatable, Codable {
/// Default behavior; add to the top of the layers stack.
case `default`
/// Layer should be positioned above the specified layer id.
case above(String)
/// Layer should be positioned below the specified layer id.
case below(String)
/// Layer should be positioned at the specified index in the layers stack.
case at(Int)
internal var corePosition: CoreLayerPosition {
switch self {
case .default:
return CoreLayerPosition()
case .above(let layerId):
return CoreLayerPosition(above: layerId)
case .below(let layerId):
return CoreLayerPosition(below: layerId)
case .at(let index):
return CoreLayerPosition(at: index)
}
}
}
レイヤーID一覧を出力する
addLayer
する際に LayerPosition
でインデックスやレイヤーIDを引数に渡して相対位置を指定できると上に書いたが、しかしそのレイヤーの階層やIDをどうやって知るのか、という問題がある。
allLayerIdentifiers
プロパティで取得できる
mapView.mapboxMap.allLayerIdentifiers.forEach { layerInfo in
print("Layer ID: \(layerInfo.id)")
}
blueprint.json スタイル使用時のレイヤー一覧:
land
waterway
water
water-gap-width
waterway copy
water-custom
land-structure-polygon
land-structure-line
water copy
ne-countries-fine
natural-earth-states
building-outline
building
tunnel-street-minor-low
tunnel-street-minor-case
tunnel-primary-secondary-tertiary-case
tunnel-major-link-case
tunnel-motorway-trunk-case
tunnel-path
tunnel-steps
tunnel-pedestrian
tunnel-major-link
tunnel-street-minor
tunnel-primary-secondary-tertiary
tunnel-motorway-trunk
road-minor-low
road-minor-case
road-street-low
road-street-case
road-secondary-tertiary-case
road-primary-case
road-major-link-case
road-motorway-trunk-case
road-path-bg
road-steps-bg
road-pedestrian-case
road-path
road-steps
road-pedestrian
road-major-link
road-minor
road-street
road-secondary-tertiary
road-primary
road-motorway-trunk
bridge-street-minor-low
bridge-street-minor-case
bridge-primary-secondary-tertiary-case
bridge-major-link-case
bridge-motorway-trunk-case
bridge-path-bg
bridge-steps-bg
bridge-pedestrian-case
bridge-path
bridge-steps
bridge-pedestrian
bridge-major-link
bridge-street-minor
bridge-primary-secondary-tertiary
bridge-motorway-trunk
bridge-major-link-2-case
bridge-motorway-trunk-2-case
bridge-major-link-2
bridge-motorway-trunk-2
admin-1-boundary-bg
admin-0-boundary-bg
admin-1-boundary
admin-0-boundary
admin-0-boundary-disputed
road-label
waterway-label
natural-line-label
natural-point-label
water-line-label
water-point-label
poi-label
settlement-subdivision-label
settlement-minor-label
settlement-major-label
puck-model-layer
3Dモデルを設置する
SceneKitを利用する方法もあるがここでは ModelLayer
というレイヤーを利用する。
ModelLayer
を利用するためには、MapboxMaps
モジュールを次のように @_spi
属性をつけてimportする必要がある:
@_spi(Experimental) import MapboxMaps
ModelLayer
を作成し、setMapStyleContent
メソッドを通じて地図に追加する。
mapView.mapboxMap.setMapStyleContent {
/// Add Models for both the duck and car using an id and a URL to the resource
Model(id: Constants.duckModelId, uri: Constants.duck)
Model(id: Constants.carModelId, uri: Constants.car)
/// Add a GeoJSONSource to the map and add the two features with geometry information
GeoJSONSource(id: Constants.sourceId)
.data(.featureCollection(FeatureCollection(features: [duckFeature, carFeature])))
/// Add a Model visualization layer which displays the two models stored in the GeoJSONSource according to the set properties
ModelLayer(id: "model-layer-id", source: Constants.sourceId)
.modelId(Exp(.get) { Constants.modelIdKey })
.modelType(.common3d)
.modelScale(x: 40, y: 40, z: 40)
.modelTranslation(x: 0, y: 0, z: 0)
.modelRotation(x: 0, y: 0, z: 90)
.modelOpacity(0.7)
}
上のコードは2種類のモデルをそれぞれ別の位置に追加する場合のコードだが、同じモデルを複数追加したい場合はこうなる:
let duckFeatures = duckCoordinates.map { point in
var duckFeature = Feature(geometry: point)
duckFeature.properties = [Constants.modelIdKey: .string(Constants.duckModelId)]
return duckFeature
}
mapView.mapboxMap.setMapStyleContent {
Model(id: Constants.duckModelId, uri: Constants.duck)
GeoJSONSource(id: Constants.sourceId)
.data(.featureCollection(FeatureCollection(features: duckFeatures)))
ModelLayer(id: "model-layer-id", source: Constants.sourceId)
...
}
外部ベクトルタイルを追加する
-
VectorSource
を追加する- 以下は Mapillary のベクトルタイルを使用している例
var vectorSource = VectorSource(id: "mapillary")
// For sources using the {z}/{x}/{y} URL scheme, use the `tiles`
// property on `VectorSource` to set the URL.
vectorSource.tiles = ["https://tiles.mapillary.com/maps/vtp/mly1_public/2/{z}/{x}/{y}?access_token=xxxx"]
vectorSource.minzoom = 6
vectorSource.maxzoom = 14
try mapView.mapboxMap.addSource(vectorSource)
- ↑の
VectorSource
を使ったレイヤーを追加する
var lineLayer = LineLayer(id: "line-layer", source: vectorSource.id)
lineLayer.sourceLayer = "sequence"
let lineColor = StyleColor(UIColor(red: 0.21, green: 0.69, blue: 0.43, alpha: 1.00))
lineLayer.lineColor = .constant(lineColor)
lineLayer.lineOpacity = .constant(0.6)
lineLayer.lineWidth = .constant(2.0)
lineLayer.lineCap = .constant(.round)
do {
try mapView.mapboxMap.addLayer(lineLayer, layerPosition: .below("waterway-label"))
} catch let layerError {
showAlert(with: layerError.localizedDescription)
}
"sequence" というレイヤー名は Mapillary のAPIドキュメントを調べるとわかる:
layer name: sequence
- zoom: 6-14 (inclusive)
- geometry: LineString
- data source: images captured in a single collection, sorted by captured_at
- properties
- captured_at, int, timestamp in ms since epoch
- creator_id, int, unique user ID of the image owner (not username)
- id, string, ID of the sequence (the legacy sequence key)
- image_id, int, ID of the "best" (first) image representing the sequence
- organization_id, int, ID of the organization this image belongs to. It can be absent
- is_pano, bool, if it is a panoramic sequence
ジオメトリが LineString であることも記載されている。
特定のエリアを抽出して可視化する
道路と立ち入り可能なエリアを抽出して着色
(これは公式サンプルにない実装)
立ち入り可能なエリアを抽出し可視化
- フィルター式の定義
- ソースの"landuse"レイヤーから、以下のものを抽出
- タイプが "Polygon"
- クラスが "park", "pedestrian", "path", "residential"
- ソースの"landuse"レイヤーから、以下のものを抽出
let landuseFilter = Exp(.all) {
Exp(.eq) {
"$type"
"Polygon"
}
Exp(.match) {
Expression(operator: .get, arguments: [.string("class")])
["park", "pedestrian", "path", "residential"]
true
false
}
}
-
FillLayer
を作成- フィルターで抽出したものを着色して可視化
var landuseLayer = FillLayer(id: "walkable-areas", source: "composite")
landuseLayer.sourceLayer = "landuse"
landuseLayer.filter = landuseFilter
landuseLayer.fillColor = .constant(.init(.green))
landuseLayer.fillOpacity = .constant(0.5)
- レイヤーをスタイルに追加
try mapView.mapboxMap.addLayer(landuseLayer)
道路を抽出し可視化
- フィルター式の定義
- ソースの "road" レイヤーからタイプが "LineString" なものを抽出 [1]
let roadFilter = Exp(.all) {
Exp(.eq) {
"$type"
"LineString"
}
}
-
LineLayer
を作成
var roadLayer = LineLayer(id: "road", source: "composite")
roadLayer.sourceLayer = "road"
roadLayer.lineColor = .constant(.init(.red))
roadLayer.lineOpacity = .constant(0.5)
roadLayer.lineWidth = .constant(3)
- レイヤーをスタイルに追加
try mapView.mapboxMap.addLayer(roadLayer)
-
"road" レイヤーはおそらく全部 "LineString" なのでこのフィルターは不要かもしれない ↩︎
Discussion