iOS Documentation: Add Fill Extrustion Layer Example (#2540)

This commit is contained in:
Bart Louwers
2024-06-22 23:41:58 +02:00
committed by GitHub
parent ab25157c0b
commit 10293028e8
6 changed files with 205 additions and 226 deletions

View File

@@ -0,0 +1,99 @@
# Fill Extrustion Layer
Add a fill extrustion layer and adjust the light dynamically with a slider.
> Note: This example uses UIKit.
This examples adds a ``MLNFillExtrusionStyleLayer``. The to be rendered height of the buildings is read from the vector data.
The global ``MLNStyle/light`` property is adjusted as the user changes a slider, which affects the fill extrustion layer.
<!-- include-example(BuildingLightExample) -->
```swift
class BuildingLightExample: UIViewController, MLNMapViewDelegate {
var mapView: MLNMapView!
var light: MLNLight!
var slider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
mapView = MLNMapView(frame: view.bounds, styleURL: AMERICANA_STYLE)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.delegate = self
// Center the map on the Flatiron Building in New York, NY.
mapView.camera = MLNMapCamera(lookingAtCenter: CLLocationCoordinate2D(latitude: 40.7411, longitude: -73.9897), altitude: 1200, pitch: 45, heading: 0)
view.addSubview(mapView)
addSlider()
}
// Add a slider to the map view. This will be used to adjust the map's light object.
func addSlider() {
slider = UISlider()
slider.translatesAutoresizingMaskIntoConstraints = false
slider.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin, .flexibleRightMargin]
slider.minimumValue = -180
slider.maximumValue = 180
slider.value = 0
slider.isContinuous = true
slider.addTarget(self, action: #selector(shiftLight), for: .valueChanged)
view.addSubview(slider)
NSLayoutConstraint.activate([
slider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40.0),
slider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40.0),
slider.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -75.0),
])
}
func mapView(_: MLNMapView, didFinishLoading style: MLNStyle) {
// Add a MLNFillExtrusionStyleLayer.
addFillExtrusionLayer(style: style)
// Create an MLNLight object.
light = MLNLight()
// Create an MLNSphericalPosition and set the radial, azimuthal, and polar values.
// Radial : Distance from the center of the base of an object to its light. Takes a CGFloat.
// Azimuthal : Position of the light relative to its anchor. Takes a CLLocationDirection.
// Polar : The height of the light. Takes a CLLocationDirection.
let position = MLNSphericalPositionMake(5, 180, 80)
light.position = NSExpression(forConstantValue: NSValue(mlnSphericalPosition: position))
// Set the light anchor to the map and add the light object to the map view's style. The light anchor can be the viewport (or rotates with the viewport) or the map (rotates with the map). To make the viewport the anchor, replace `map` with `viewport`.
light.anchor = NSExpression(forConstantValue: "map")
style.light = light
}
@objc func shiftLight() {
// Use the slider's value to change the light's polar value.
let position = MLNSphericalPositionMake(5, 180, CLLocationDirection(slider.value))
light.position = NSExpression(forConstantValue: NSValue(mlnSphericalPosition: position))
mapView.style?.light = light
}
func addFillExtrusionLayer(style: MLNStyle) {
// Access the OpenMapTiles source and use it to create a `MLNFillExtrusionStyleLayer`. The source identifier is `openmaptiles`. Use the `sources` property on a style to verify source identifiers.
guard let source = style.source(withIdentifier: "openmaptiles") else {
print("Could not find source openmaptiles")
return
}
let layer = MLNFillExtrusionStyleLayer(identifier: "extrusion-layer", source: source)
layer.sourceLayerIdentifier = "building"
layer.fillExtrusionBase = NSExpression(forKeyPath: "render_min_height")
layer.fillExtrusionHeight = NSExpression(forKeyPath: "render_height")
layer.fillExtrusionOpacity = NSExpression(forConstantValue: 0.8)
layer.fillExtrusionColor = NSExpression(forConstantValue: UIColor.white)
// Access the map's layer with the identifier "poi" and insert the fill extrusion layer below it.
let symbolLayer = style.layer(withIdentifier: "poi")!
style.insertLayer(layer, below: symbolLayer)
}
}
```
![](BuildingLightExample.png)

View File

@@ -25,6 +25,7 @@ Powerful, free and open-source mapping toolkit with full control over data sourc
- <doc:WebAPIDataExample>
- <doc:AnimatedLineExample>
- <doc:AnnotationViewExample>
- <doc:BuildingLightExample>
## Topics

View File

@@ -1,224 +0,0 @@
# Showing data from an API (UIKit)
Showing data from an API with custom styling and interaction
This example loads lighthouses in the United States from [WikiData](http://tinyurl.com/zrl2jc4). It adds points to the map and applies dynamic styling to these points. When zooming in the dots become larger circles with a custom icon and the name of the lighthouse shown next to it. When tapping a callout is shown with the name of the lighthouse that was tapped on.
<!-- include-example(WebAPIDataExampleUIKit) -->
```swift
class WebAPIDataExampleUIKit: UIViewController, MLNMapViewDelegate {
var mapView: MLNMapView!
override func viewDidLoad() {
super.viewDidLoad()
mapView = MLNMapView(frame: view.bounds)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.setCenter(CLLocationCoordinate2D(latitude: 37.090240, longitude: -95.712891), zoomLevel: 2, animated: false)
mapView.delegate = self
mapView.attributionButton.isHidden = true
view.addSubview(mapView)
// Add a single tap gesture recognizer. This gesture requires the built-in MLNMapView tap gestures (such as those for zoom and annotation selection) to fail.
let singleTap = UITapGestureRecognizer(target: self, action: #selector(handleMapTap(sender:)))
for recognizer in mapView.gestureRecognizers! where recognizer is UITapGestureRecognizer {
singleTap.require(toFail: recognizer)
}
mapView.addGestureRecognizer(singleTap)
}
func mapView(_: MLNMapView, didFinishLoading _: MLNStyle) {
fetchPoints { [weak self] features in
self?.addItemsToMap(features: features)
}
}
func addItemsToMap(features: [MLNPointFeature]) {
// MLNMapView.style is optional, so you must guard against it not being set.
guard let style = mapView.style else { return }
// You can add custom UIImages to the map style.
// These can be referenced by an MLNSymbolStyleLayer’s iconImage property.
style.setImage(UIImage(named: "lighthouse")!, forName: "lighthouse")
// Add the features to the map as a shape source.
let source = MLNShapeSource(identifier: "us-lighthouses", features: features, options: nil)
style.addSource(source)
let lighthouseColor = UIColor(red: 0.08, green: 0.44, blue: 0.96, alpha: 1.0)
// Use MLNCircleStyleLayer to represent the points with simple circles.
// In this case, we can use style functions to gradually change properties between zoom level 2 and 7: the circle opacity from 50% to 100% and the circle radius from 2pt to 3pt.
let circles = MLNCircleStyleLayer(identifier: "lighthouse-circles", source: source)
circles.circleColor = NSExpression(forConstantValue: lighthouseColor)
// The circles should increase in opacity from 0.5 to 1 based on zoom level.
circles.circleOpacity = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [2: 0.5, 7: 1]))
circles.circleRadius = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [2: 2, 7: 3]))
// Use MLNSymbolStyleLayer for more complex styling of points including custom icons and text rendering.
let symbols = MLNSymbolStyleLayer(identifier: "lighthouse-symbols", source: source)
symbols.iconImageName = NSExpression(forConstantValue: "lighthouse")
symbols.iconColor = NSExpression(forConstantValue: lighthouseColor)
symbols.iconScale = NSExpression(forConstantValue: 0.5)
symbols.iconHaloColor = NSExpression(forConstantValue: UIColor.white.withAlphaComponent(0.5))
symbols.iconHaloWidth = NSExpression(forConstantValue: 1)
symbols.iconOpacity = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [5.9: 0, 6: 1]))
// "name" references the "name" key in an MLNPointFeature’s attributes dictionary.
symbols.text = NSExpression(forKeyPath: "name")
symbols.textColor = symbols.iconColor
symbols.textFontSize = NSExpression(forMLNInterpolating: NSExpression.zoomLevelVariable, curveType: MLNExpressionInterpolationMode.linear, parameters: nil, stops: NSExpression(forConstantValue: [10: 10, 16: 16]))
symbols.textTranslation = NSExpression(forConstantValue: NSValue(cgVector: CGVector(dx: 15, dy: 0)))
symbols.textOpacity = symbols.iconOpacity
symbols.textHaloColor = symbols.iconHaloColor
symbols.textHaloWidth = symbols.iconHaloWidth
symbols.textJustification = NSExpression(forConstantValue: NSValue(mlnTextJustification: .left))
symbols.textAnchor = NSExpression(forConstantValue: NSValue(mlnTextAnchor: .left))
style.addLayer(circles)
style.addLayer(symbols)
}
// MARK: - Feature interaction
@IBAction func handleMapTap(sender: UITapGestureRecognizer) {
if sender.state == .ended {
// Limit feature selection to just the following layer identifiers.
let layerIdentifiers: Set = ["lighthouse-symbols", "lighthouse-circles"]
// Try matching the exact point first.
let point = sender.location(in: sender.view!)
for feature in mapView.visibleFeatures(at: point, styleLayerIdentifiers: layerIdentifiers)
where feature is MLNPointFeature
{
guard let selectedFeature = feature as? MLNPointFeature else {
fatalError("Failed to cast selected feature as MLNPointFeature")
}
showCallout(feature: selectedFeature)
return
}
let touchCoordinate = mapView.convert(point, toCoordinateFrom: sender.view!)
let touchLocation = CLLocation(latitude: touchCoordinate.latitude, longitude: touchCoordinate.longitude)
// Otherwise, get all features within a rect the size of a touch (44x44).
let touchRect = CGRect(origin: point, size: .zero).insetBy(dx: -22.0, dy: -22.0)
let possibleFeatures = mapView.visibleFeatures(in: touchRect, styleLayerIdentifiers: Set(layerIdentifiers)).filter { $0 is MLNPointFeature }
// Select the closest feature to the touch center.
let closestFeatures = possibleFeatures.sorted(by: {
CLLocation(latitude: $0.coordinate.latitude, longitude: $0.coordinate.longitude).distance(from: touchLocation) < CLLocation(latitude: $1.coordinate.latitude, longitude: $1.coordinate.longitude).distance(from: touchLocation)
})
if let feature = closestFeatures.first {
guard let closestFeature = feature as? MLNPointFeature else {
fatalError("Failed to cast selected feature as MLNPointFeature")
}
showCallout(feature: closestFeature)
return
}
// If no features were found, deselect the selected annotation, if any.
mapView.deselectAnnotation(mapView.selectedAnnotations.first, animated: true)
}
}
func showCallout(feature: MLNPointFeature) {
let point = MLNPointFeature()
point.title = feature.attributes["name"] as? String
point.coordinate = feature.coordinate
// Selecting an feature that doesn’t already exist on the map will add a new annotation view.
// We’ll need to use the map’s delegate methods to add an empty annotation view and remove it when we’re done selecting it.
mapView.selectAnnotation(point, animated: true, completionHandler: nil)
}
// MARK: - MLNMapViewDelegate
func mapView(_: MLNMapView, annotationCanShowCallout _: MLNAnnotation) -> Bool {
true
}
func mapView(_ mapView: MLNMapView, didDeselect annotation: MLNAnnotation) {
mapView.removeAnnotations([annotation])
}
func mapView(_: MLNMapView, viewFor _: MLNAnnotation) -> MLNAnnotationView? {
// Create an empty view annotation. Set a frame to offset the callout.
MLNAnnotationView(frame: CGRect(x: 0, y: 0, width: 20, height: 20))
}
// MARK: - Data fetching and parsing
func fetchPoints(withCompletion completion: @escaping (([MLNPointFeature]) -> Void)) {
// Wikidata query for all lighthouses in the United States: http://tinyurl.com/zrl2jc4
let query = "SELECT DISTINCT ?item " +
"?itemLabel ?coor ?image " +
"WHERE " +
"{ " +
"?item wdt:P31 wd:Q39715 . " +
"?item wdt:P17 wd:Q30 . " +
"?item wdt:P625 ?coor . " +
"OPTIONAL { ?item wdt:P18 ?image } . " +
"SERVICE wikibase:label { bd:serviceParam wikibase:language \"en\" } " +
"} " +
"ORDER BY ?itemLabel"
let characterSet = NSMutableCharacterSet()
characterSet.formUnion(with: CharacterSet.urlQueryAllowed)
characterSet.removeCharacters(in: "?")
characterSet.removeCharacters(in: "&")
characterSet.removeCharacters(in: ":")
let encodedQuery = query.addingPercentEncoding(withAllowedCharacters: characterSet as CharacterSet)!
let request = URLRequest(url: URL(string: "https://query.wikidata.org/sparql?query=\(encodedQuery)&format=json")!)
URLSession.shared.dataTask(with: request, completionHandler: { data, _, error in
guard error == nil else {
preconditionFailure("Failed to load GeoJSON data: \(error!)")
}
guard
let data,
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: AnyObject],
let results = json["results"] as? [String: AnyObject],
let items = results["bindings"] as? [[String: AnyObject]]
else {
preconditionFailure("Failed to parse GeoJSON data")
}
DispatchQueue.main.async {
completion(self.parseJSONItems(items: items))
}
}).resume()
}
func parseJSONItems(items: [[String: AnyObject]]) -> [MLNPointFeature] {
var features = [MLNPointFeature]()
for item in items {
guard let label = item["itemLabel"] as? [String: AnyObject],
let title = label["value"] as? String else { continue }
guard let coor = item["coor"] as? [String: AnyObject],
let point = coor["value"] as? String else { continue }
let parsedPoint = point.replacingOccurrences(of: "Point(", with: "").replacingOccurrences(of: ")", with: "")
let pointComponents = parsedPoint.components(separatedBy: " ")
let coordinate = CLLocationCoordinate2D(latitude: Double(pointComponents[1])!, longitude: Double(pointComponents[0])!)
let feature = MLNPointFeature()
feature.coordinate = coordinate
feature.title = title
// A feature’s attributes can used by runtime styling for things like text labels.
feature.attributes = [
"name": title,
]
features.append(feature)
}
return features
}
}
```
![](WebAPIDataExample.png)

View File

@@ -0,0 +1,99 @@
import MapLibre
import SwiftUI
import UIKit
// #-example-code(BuildingLightExample)
class BuildingLightExample: UIViewController, MLNMapViewDelegate {
var mapView: MLNMapView!
var light: MLNLight!
var slider: UISlider!
override func viewDidLoad() {
super.viewDidLoad()
mapView = MLNMapView(frame: view.bounds, styleURL: AMERICANA_STYLE)
mapView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
mapView.delegate = self
// Center the map on the Flatiron Building in New York, NY.
mapView.camera = MLNMapCamera(lookingAtCenter: CLLocationCoordinate2D(latitude: 40.7411, longitude: -73.9897), altitude: 1200, pitch: 45, heading: 0)
view.addSubview(mapView)
addSlider()
}
// Add a slider to the map view. This will be used to adjust the map's light object.
func addSlider() {
slider = UISlider()
slider.translatesAutoresizingMaskIntoConstraints = false
slider.autoresizingMask = [.flexibleTopMargin, .flexibleLeftMargin, .flexibleRightMargin]
slider.minimumValue = -180
slider.maximumValue = 180
slider.value = 0
slider.isContinuous = true
slider.addTarget(self, action: #selector(shiftLight), for: .valueChanged)
view.addSubview(slider)
NSLayoutConstraint.activate([
slider.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 40.0),
slider.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -40.0),
slider.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: -75.0),
])
}
func mapView(_: MLNMapView, didFinishLoading style: MLNStyle) {
// Add a MLNFillExtrusionStyleLayer.
addFillExtrusionLayer(style: style)
// Create an MLNLight object.
light = MLNLight()
// Create an MLNSphericalPosition and set the radial, azimuthal, and polar values.
// Radial : Distance from the center of the base of an object to its light. Takes a CGFloat.
// Azimuthal : Position of the light relative to its anchor. Takes a CLLocationDirection.
// Polar : The height of the light. Takes a CLLocationDirection.
let position = MLNSphericalPositionMake(5, 180, 80)
light.position = NSExpression(forConstantValue: NSValue(mlnSphericalPosition: position))
// Set the light anchor to the map and add the light object to the map view's style. The light anchor can be the viewport (or rotates with the viewport) or the map (rotates with the map). To make the viewport the anchor, replace `map` with `viewport`.
light.anchor = NSExpression(forConstantValue: "map")
style.light = light
}
@objc func shiftLight() {
// Use the slider's value to change the light's polar value.
let position = MLNSphericalPositionMake(5, 180, CLLocationDirection(slider.value))
light.position = NSExpression(forConstantValue: NSValue(mlnSphericalPosition: position))
mapView.style?.light = light
}
func addFillExtrusionLayer(style: MLNStyle) {
// Access the OpenMapTiles source and use it to create a `MLNFillExtrusionStyleLayer`. The source identifier is `openmaptiles`. Use the `sources` property on a style to verify source identifiers.
guard let source = style.source(withIdentifier: "openmaptiles") else {
print("Could not find source openmaptiles")
return
}
let layer = MLNFillExtrusionStyleLayer(identifier: "extrusion-layer", source: source)
layer.sourceLayerIdentifier = "building"
layer.fillExtrusionBase = NSExpression(forKeyPath: "render_min_height")
layer.fillExtrusionHeight = NSExpression(forKeyPath: "render_height")
layer.fillExtrusionOpacity = NSExpression(forConstantValue: 0.8)
layer.fillExtrusionColor = NSExpression(forConstantValue: UIColor.white)
// Access the map's layer with the identifier "poi" and insert the fill extrusion layer below it.
let symbolLayer = style.layer(withIdentifier: "poi")!
style.insertLayer(layer, below: symbolLayer)
}
}
// #-end-example-code
struct BuildingLightExampleUIViewControllerRepresentable: UIViewControllerRepresentable {
typealias UIViewControllerType = BuildingLightExample
func makeUIViewController(context _: Context) -> BuildingLightExample {
BuildingLightExample()
}
func updateUIViewController(_: BuildingLightExample, context _: Context) {}
}

View File

@@ -32,6 +32,9 @@ struct MapLibreNavigationView: View {
NavigationLink("AnnotationViewExample") {
AnnotationViewExampleUIViewControllerRepresentable()
}
NavigationLink("BuildingLightExample") {
BuildingLightExampleUIViewControllerRepresentable()
}
}
}
}

View File

@@ -59,7 +59,7 @@ filter_filenames() {
ios_headers=$(filter_filenames "platform/ios/src" "$public_headers")
darwin_headers=$(filter_filenames "platform/darwin/src" "$public_headers")
docc_options=(
clang_options=(
--toolchain swift clang
-extract-api
--product-name=MapLibre
@@ -73,7 +73,7 @@ docc_options=(
headers=($ios_headers $darwin_headers build/headers/*.h)
output_name="combined.symbols.json"
xcrun "${docc_options[@]}" \
xcrun "${clang_options[@]}" \
-o "$build_dir/symbol-graphs/$output_name" \
"${headers[@]}"
@@ -86,6 +86,7 @@ $(xcrun --find docc) "$cmd" platform/ios/MapLibre.docc \
--source-service github \
--source-service-base-url https://github.com/maplibre/maplibre-native/blob/main \
--checkout-path $(realpath .) \
${HOSTING_BASE_PATH:+--hosting-base-path "$HOSTING_BASE_PATH"} \
--output-path "$build_dir"/MapLibre.doccarchive
if [[ "$cmd" == "convert" ]]; then