diff --git a/platform/ios/MapLibre.docc/BuildingLightExample.md b/platform/ios/MapLibre.docc/BuildingLightExample.md new file mode 100644 index 000000000..d5741191d --- /dev/null +++ b/platform/ios/MapLibre.docc/BuildingLightExample.md @@ -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. + + + + +```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) \ No newline at end of file diff --git a/platform/ios/MapLibre.docc/MapLibre.md b/platform/ios/MapLibre.docc/MapLibre.md index eccb843ac..943e251ec 100644 --- a/platform/ios/MapLibre.docc/MapLibre.md +++ b/platform/ios/MapLibre.docc/MapLibre.md @@ -25,6 +25,7 @@ Powerful, free and open-source mapping toolkit with full control over data sourc - - - +- ## Topics diff --git a/platform/ios/MapLibre.docc/WebAPIDataExampleUIKit.md b/platform/ios/MapLibre.docc/WebAPIDataExampleUIKit.md deleted file mode 100644 index 629f4d3b4..000000000 --- a/platform/ios/MapLibre.docc/WebAPIDataExampleUIKit.md +++ /dev/null @@ -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. - - - -```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) \ No newline at end of file diff --git a/platform/ios/app-swift/Sources/BuildingLightExample.swift b/platform/ios/app-swift/Sources/BuildingLightExample.swift new file mode 100644 index 000000000..72d7d1b56 --- /dev/null +++ b/platform/ios/app-swift/Sources/BuildingLightExample.swift @@ -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) {} +} diff --git a/platform/ios/app-swift/Sources/MapLibreNavigationView.swift b/platform/ios/app-swift/Sources/MapLibreNavigationView.swift index 9bbffbfa6..ae12528f5 100644 --- a/platform/ios/app-swift/Sources/MapLibreNavigationView.swift +++ b/platform/ios/app-swift/Sources/MapLibreNavigationView.swift @@ -32,6 +32,9 @@ struct MapLibreNavigationView: View { NavigationLink("AnnotationViewExample") { AnnotationViewExampleUIViewControllerRepresentable() } + NavigationLink("BuildingLightExample") { + BuildingLightExampleUIViewControllerRepresentable() + } } } } diff --git a/platform/ios/scripts/docc.sh b/platform/ios/scripts/docc.sh index c085b821b..1d054d78b 100755 --- a/platform/ios/scripts/docc.sh +++ b/platform/ios/scripts/docc.sh @@ -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