Skip to content

Commit b7ae0d1

Browse files
authored
Support Base and Height alignment in FillExtrudeLayer; zOffset for FillLayer and Polygon Annotations (#2351)
1 parent 1cf9515 commit b7ae0d1

File tree

20 files changed

+512
-39
lines changed

20 files changed

+512
-39
lines changed

Apps/Examples/Examples/All Examples/BuildingExtrusionsExample.swift

Lines changed: 152 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,91 @@
11
import UIKit
22
@_spi(Experimental) import MapboxMaps
33

4-
final class BuildingExtrusionsExample: UIViewController, ExampleProtocol {
5-
private var cancelables = Set<AnyCancelable>()
6-
7-
private lazy var lightPositionButton: UIButton = {
8-
let button = UIButton(type: .system)
4+
extension UIButton {
5+
static func exampleActionButton() -> UIButton {
6+
let button = UIButton(type: .custom)
97
button.translatesAutoresizingMaskIntoConstraints = false
108
button.backgroundColor = .systemBlue
119
button.tintColor = .white
1210
button.layer.cornerRadius = 4
1311
button.clipsToBounds = true
14-
button.contentEdgeInsets = UIEdgeInsets(top: 4, left: 4, bottom: 4, right: 4)
15-
if #available(iOS 13.0, *) {
12+
button.contentEdgeInsets = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
13+
return button
14+
}
15+
}
16+
17+
final class BuildingExtrusionsExample: UIViewController, ExampleProtocol {
18+
private var cancelables = Set<AnyCancelable>()
19+
20+
private lazy var lightPositionButton: UIButton = {
21+
let button = UIButton.exampleActionButton()
22+
if #available(iOS 13.1, *) {
1623
button.setImage(UIImage(systemName: "flashlight.on.fill"), for: .normal)
1724
} else {
1825
button.setTitle("Position", for: .normal)
1926
}
20-
button.addTarget(self, action: #selector(lightPositionButtonTapped(_:)), for: .touchUpInside)
27+
button.addTarget(self, action: #selector(lightPositionButtonTapped(_:)), for: .primaryActionTriggered)
2128
return button
2229
}()
2330

2431
private lazy var lightColorButton: UIButton = {
25-
let button = UIButton(type: .system)
26-
button.translatesAutoresizingMaskIntoConstraints = false
27-
button.backgroundColor = .systemBlue
28-
button.tintColor = .white
29-
button.layer.cornerRadius = 4
30-
button.clipsToBounds = true
31-
button.contentEdgeInsets = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
32+
let button = UIButton.exampleActionButton()
3233
if #available(iOS 13.0, *) {
3334
button.setImage(UIImage(systemName: "paintbrush.fill"), for: .normal)
3435
} else {
3536
button.setTitle("Color", for: .normal)
3637
}
37-
button.addTarget(self, action: #selector(lightColorButtonTapped(_:)), for: .touchUpInside)
38+
button.addTarget(self, action: #selector(lightColorButtonTapped(_:)), for: .primaryActionTriggered)
3839
return button
3940
}()
4041

42+
private lazy var heightAlignmentButton: UIButton = {
43+
let button = UIButton.exampleActionButton()
44+
45+
if #available(iOS 15.0, *) {
46+
button.setImage(UIImage(systemName: "align.vertical.top"), for: .normal)
47+
button.setImage(UIImage(systemName: "align.vertical.top.fill"), for: .selected)
48+
} else {
49+
button.setTitle("Height Alignment", for: .normal)
50+
}
51+
button.addTarget(self, action: #selector(heightAlignmentButtonTapped(_:)), for: .primaryActionTriggered)
52+
return button
53+
}()
54+
55+
private lazy var baseAlignmentButton: UIButton = {
56+
let button = UIButton.exampleActionButton()
57+
58+
if #available(iOS 15.0, *) {
59+
button.setImage(UIImage(systemName: "align.vertical.bottom"), for: .normal)
60+
button.setImage(UIImage(systemName: "align.vertical.bottom.fill"), for: .selected)
61+
} else {
62+
button.setTitle("Height Alignment", for: .normal)
63+
}
64+
button.addTarget(self, action: #selector(baseAlignmentButtonTapped(_:)), for: .primaryActionTriggered)
65+
return button
66+
}()
67+
68+
private lazy var terrainSwitchButton: UIButton = {
69+
let button = UIButton.exampleActionButton()
70+
if #available(iOS 15.0, *) {
71+
button.setImage(UIImage(systemName: "mountain.2"), for: .normal)
72+
button.setImage(UIImage(systemName: "mountain.2.fill"), for: .selected)
73+
} else {
74+
button.setTitle("Terrain", for: .normal)
75+
}
76+
77+
button.addTarget(self, action: #selector(terrainButtonTapped(_:)), for: .primaryActionTriggered)
78+
return button
79+
}()
80+
81+
lazy var buttons = [
82+
heightAlignmentButton,
83+
baseAlignmentButton,
84+
lightPositionButton,
85+
lightColorButton,
86+
terrainSwitchButton
87+
]
88+
4189
private var ambientLight: AmbientLight = {
4290
var light = AmbientLight()
4391
light.color = .constant(StyleColor(.blue))
@@ -68,21 +116,27 @@ final class BuildingExtrusionsExample: UIViewController, ExampleProtocol {
68116
self.setupExample()
69117
}.store(in: &cancelables)
70118

71-
view.addSubview(lightPositionButton)
72-
view.addSubview(lightColorButton)
119+
120+
buttons.forEach(view.addSubview(_:))
121+
terrainSwitchButton.isSelected = isTerrainEnabled
122+
123+
let accessoryButtonsStackView = UIStackView(arrangedSubviews: buttons)
124+
accessoryButtonsStackView.axis = .vertical
125+
accessoryButtonsStackView.spacing = 20
126+
accessoryButtonsStackView.translatesAutoresizingMaskIntoConstraints = false
127+
128+
view.addSubview(accessoryButtonsStackView)
73129

74130
NSLayoutConstraint.activate([
75-
view.trailingAnchor.constraint(equalToSystemSpacingAfter: lightPositionButton.trailingAnchor, multiplier: 1),
76-
view.bottomAnchor.constraint(equalTo: lightPositionButton.bottomAnchor, constant: 100),
77-
view.trailingAnchor.constraint(equalToSystemSpacingAfter: lightColorButton.trailingAnchor, multiplier: 1),
78-
lightPositionButton.topAnchor.constraint(equalToSystemSpacingBelow: lightColorButton.bottomAnchor, multiplier: 1),
79-
lightColorButton.widthAnchor.constraint(equalTo: lightPositionButton.widthAnchor),
80-
lightColorButton.heightAnchor.constraint(equalTo: lightPositionButton.heightAnchor)
131+
mapView.ornaments.attributionButton.topAnchor.constraint(equalToSystemSpacingBelow: accessoryButtonsStackView.bottomAnchor, multiplier: 1),
132+
view.trailingAnchor
133+
.constraint(equalToSystemSpacingAfter: accessoryButtonsStackView.trailingAnchor, multiplier: 1)
81134
])
82135
}
83136

84137
internal func setupExample() {
85-
addBuildingExtrusions()
138+
try! addTerrain()
139+
try! addBuildingExtrusions()
86140

87141
let cameraOptions = CameraOptions(center: CLLocationCoordinate2D(latitude: 40.7135, longitude: -74.0066),
88142
zoom: 15.5,
@@ -97,18 +151,20 @@ final class BuildingExtrusionsExample: UIViewController, ExampleProtocol {
97151
}
98152

99153
// See https://docs.mapbox.com/mapbox-gl-js/example/3d-buildings/ for equivalent gl-js example
100-
internal func addBuildingExtrusions() {
154+
internal func addBuildingExtrusions() throws {
101155
let wallOnlyThreshold = 20
102156
let extrudeFilter = Exp(.eq) {
103157
Exp(.get) { "extrude" }
104158
"true"
105159
}
106160
var layer = FillExtrusionLayer(id: "3d-buildings", source: "composite")
161+
.minZoom(15)
162+
.sourceLayer("building")
163+
.fillExtrusionColor(.lightGray)
164+
.fillExtrusionOpacity(0.8)
165+
.fillExtrusionAmbientOcclusionIntensity(0.3)
166+
.fillExtrusionAmbientOcclusionRadius(3.0)
107167

108-
layer.minZoom = 15
109-
layer.sourceLayer = "building"
110-
layer.fillExtrusionColor = .constant(StyleColor(.lightGray))
111-
layer.fillExtrusionOpacity = .constant(0.6)
112168

113169
layer.filter = Exp(.all) {
114170
extrudeFilter
@@ -141,13 +197,10 @@ final class BuildingExtrusionsExample: UIViewController, ExampleProtocol {
141197
}
142198
)
143199

144-
layer.fillExtrusionAmbientOcclusionIntensity = .constant(0.3)
145-
146-
layer.fillExtrusionAmbientOcclusionRadius = .constant(3.0)
147-
148-
try! mapView.mapboxMap.addLayer(layer)
200+
try mapView.mapboxMap.addLayer(layer)
149201

150202
var wallsOnlyExtrusionLayer = layer
203+
.fillExtrusionLineWidth(2)
151204
wallsOnlyExtrusionLayer.id = "3d-buildings-wall"
152205
wallsOnlyExtrusionLayer.filter = Exp(.all) {
153206
extrudeFilter
@@ -157,13 +210,75 @@ final class BuildingExtrusionsExample: UIViewController, ExampleProtocol {
157210
}
158211
}
159212

160-
wallsOnlyExtrusionLayer.fillExtrusionLineWidth = .constant(2)
213+
try mapView.mapboxMap.addLayer(wallsOnlyExtrusionLayer)
214+
}
161215

162-
try! mapView.mapboxMap.addLayer(wallsOnlyExtrusionLayer)
216+
func addTerrain() throws {
217+
let terrainSourceID = "mapbox-dem"
218+
219+
if !mapView.mapboxMap.sourceExists(withId: terrainSourceID) {
220+
try addTerrainSource(id: terrainSourceID)
221+
}
222+
223+
try mapView.mapboxMap.setTerrain(Terrain(sourceId: terrainSourceID)
224+
.exaggeration(1.5))
225+
}
226+
227+
func addTerrainSource(id: String) throws {
228+
var demSource = RasterDemSource(id: id)
229+
demSource.url = "mapbox://mapbox.mapbox-terrain-dem-v1"
230+
// Setting the `tileSize` to 514 provides better performance and adds padding around the outside
231+
// of the tiles.
232+
demSource.tileSize = 514
233+
demSource.maxzoom = 14.0
234+
try mapView.mapboxMap.addSource(demSource)
163235
}
164236

165237
// MARK: - Actions
166238

239+
var isTerrainEnabled = true
240+
241+
@objc private func terrainButtonTapped(_ sender: UIButton) {
242+
if isTerrainEnabled {
243+
mapView.mapboxMap.removeTerrain()
244+
} else {
245+
try! addTerrain()
246+
}
247+
248+
isTerrainEnabled.toggle()
249+
sender.isSelected = isTerrainEnabled
250+
}
251+
252+
var baseAlignment: FillExtrusionBaseAlignment = .flat
253+
var heightAlignment: FillExtrusionHeightAlignment = .flat
254+
255+
@objc private func baseAlignmentButtonTapped(_ sender: UIButton) {
256+
if baseAlignment == .flat {
257+
baseAlignment = .terrain
258+
} else {
259+
baseAlignment = .flat
260+
}
261+
sender.backgroundColor = .systemBlue
262+
sender.isSelected = baseAlignment == .terrain
263+
264+
try! mapView.mapboxMap.updateLayer(withId: "3d-buildings", type: FillExtrusionLayer.self) { layer in
265+
layer.fillExtrusionBaseAlignment = .constant(baseAlignment)
266+
}
267+
}
268+
269+
@objc private func heightAlignmentButtonTapped(_ sender: UIButton) {
270+
if heightAlignment == .flat {
271+
heightAlignment = .terrain
272+
} else {
273+
heightAlignment = .flat
274+
}
275+
sender.isSelected = heightAlignment == .terrain
276+
277+
try! mapView.mapboxMap.updateLayer(withId: "3d-buildings", type: FillExtrusionLayer.self) { layer in
278+
layer.fillExtrusionHeightAlignment = .constant(heightAlignment)
279+
}
280+
}
281+
167282
@objc private func lightColorButtonTapped(_ sender: UIButton) {
168283
if case .constant(let color) = ambientLight.color, color == StyleColor(.red) {
169284
ambientLight.color = .constant(StyleColor(.blue))

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ Mapbox welcomes participation and contributions from everyone.
55
## main
66

77
* Add two separete Geofence examples in SwiftUI - `GeofencingPlayground` and `GeofencingUserLocation`
8+
* Add support for Base and Height alignment in FillExtrusionLayer.
9+
* Add support for `pitchAlignment` in BackgroundLayer.
10+
* Add support for `zOffset` in FillLayer, PolygonAnnotation[Manager] and PolygonAnnotationGroup.
811

912
## 11.8.0-rc.1 - 23 October, 2024
1013

Sources/MapboxMaps/Annotations/Generated/PolygonAnnotation.swift

Lines changed: 11 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/MapboxMaps/Annotations/Generated/PolygonAnnotationManager.swift

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Sources/MapboxMaps/Documentation.docc/API Catalogs/Layer Property Values.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@
4242
- ``TransitionOptions-struct``
4343
- ``ModelScaleMode``
4444
- ``SymbolElevationReference``
45+
- ``FillExtrusionBaseAlignment``
46+
- ``FillExtrusionHeightAlignment``
47+
- ``BackgroundPitchAlignment``
4548

4649
<!-- Next two are arguable regarding it's category -->
4750
- ``ImageContent``

Sources/MapboxMaps/Style/Generated/Layers/BackgroundLayer.swift

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)